From 05bf6aba6a79f879405d874655528db64041a33f Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Thu, 9 Feb 2023 11:08:18 +1100 Subject: [PATCH] Add getPayloadBodiesByRangeV1 and getPayloadBodiesByHash engine methods (#4980) * Add engine get payload body methods and test Signed-off-by: Zhenyang Shi Signed-off-by: Gabriel Fukushima * Add header Signed-off-by: Zhenyang Shi * Update result struct & add test Signed-off-by: Zhenyang Shi * Change constant to use upper case Signed-off-by: Gabriel Fukushima * Add PayloadBody class and withdrawals to response of methods Signed-off-by: Gabriel Fukushima * Add unit tests Signed-off-by: Gabriel Fukushima * Add changelog Signed-off-by: Gabriel Fukushima * spotless Signed-off-by: Gabriel Fukushima * Add check to prevent returning trailing null results past the head Signed-off-by: Gabriel Fukushima * Add test to check trailing null post head scenario Signed-off-by: Gabriel Fukushima * Split tests into pre and post shanghai Signed-off-by: Gabriel Fukushima * spotless Signed-off-by: Gabriel Fukushima * Rename methods Signed-off-by: Gabriel Fukushima * Use getName() to log method name Signed-off-by: Gabriel Fukushima * spotless Signed-off-by: Gabriel Fukushima * Rename variable Signed-off-by: Gabriel Fukushima * Call constructor directly Signed-off-by: Gabriel Fukushima * Fix ByHash json parsing Signed-off-by: Simon Dudley * Fix json parsing again Signed-off-by: Simon Dudley * Add check to prevent unnecessary queries Signed-off-by: Gabriel Fukushima * Refactor method Signed-off-by: Gabriel Fukushima * Add new error code for EngineGetPayloadBodies methods Signed-off-by: Gabriel Fukushima * Add return error for request above the API limit Signed-off-by: Gabriel Fukushima * Add constructor for empty response Signed-off-by: Gabriel Fukushima * add check for number of blocks requested and for requests of post head Signed-off-by: Gabriel Fukushima * Add test to check error code when request exceeds API limits Signed-off-by: Gabriel Fukushima * add constant for max blocks allowed per request Signed-off-by: Gabriel Fukushima * spotless Signed-off-by: Gabriel Fukushima * Fix some nits Signed-off-by: Gabriel Fukushima * Add invalid params check Signed-off-by: Gabriel Fukushima * Add tests for invalid params check Signed-off-by: Gabriel Fukushima * Fix test and spotless Signed-off-by: Gabriel Fukushima * Revert "Fix json parsing again" This reverts commit 558d325bf3dad474392a1c1f6b459a6d8178fb6c. Signed-off-by: Gabriel Fukushima * Revert "Fix ByHash json parsing Signed-off-by: Simon Dudley " This reverts commit 663e11e2 Signed-off-by: Gabriel Fukushima * Use UnsignedLongParameter to cast params of the request Signed-off-by: Gabriel Fukushima * Add optional withdrawals to the NewPayload log (#5021) Signed-off-by: Simon Dudley * kubernetes and errorprone - update versions (#5013) * update errorprone and kubernetes versions * fixed errorprone issues in prod cod * fixed errorprone issues in test code --------- Signed-off-by: Sally MacFarlane * Rename JsonRpcService to EngineJsonRpcService (#5036) * rename JsonRpcService to EngineJsonRpcService Signed-off-by: Daniel Lehrner * Params should be single item of array type, not outer array of strings (#5037) Signed-off-by: Simon Dudley * Add EIP-2537 (BLS12-381 curve precompiles) to Cancun (#5017) Add the BLS curve precompiles into the registry for cancun. All of the curve precompiles have been here since berlin, so this is just wiring them in. Signed-off-by: Danno Ferrin Co-authored-by: Sally MacFarlane * Use UnsignedLongParameter to cast params of the request Signed-off-by: Gabriel Fukushima * Add optional withdrawals to the NewPayload log (#5021) Signed-off-by: Simon Dudley * kubernetes and errorprone - update versions (#5013) * update errorprone and kubernetes versions * fixed errorprone issues in prod cod * fixed errorprone issues in test code --------- Signed-off-by: Sally MacFarlane * Rename JsonRpcService to EngineJsonRpcService (#5036) * rename JsonRpcService to EngineJsonRpcService Signed-off-by: Daniel Lehrner * Params should be single item of array type, not outer array of strings (#5037) Signed-off-by: Simon Dudley * Add EIP-2537 (BLS12-381 curve precompiles) to Cancun (#5017) Add the BLS curve precompiles into the registry for cancun. All of the curve precompiles have been here since berlin, so this is just wiring them in. Signed-off-by: Danno Ferrin Co-authored-by: Sally MacFarlane * Convert start and count from hex to match JSON-RPC Spec standard Signed-off-by: Gabriel Fukushima --------- Signed-off-by: Zhenyang Shi Signed-off-by: Gabriel Fukushima Signed-off-by: Simon Dudley Signed-off-by: Sally MacFarlane Signed-off-by: Daniel Lehrner Signed-off-by: Danno Ferrin Co-authored-by: Zhenyang Shi Co-authored-by: Simon Dudley Co-authored-by: Sally MacFarlane Co-authored-by: Daniel Lehrner Co-authored-by: Danno Ferrin --- CHANGELOG.md | 2 + .../besu/ethereum/api/jsonrpc/RpcMethod.java | 2 + .../EngineGetPayloadBodiesByHashV1.java | 85 +++++ .../EngineGetPayloadBodiesByRangeV1.java | 113 ++++++ .../internal/response/JsonRpcError.java | 2 +- .../internal/results/BlockResultFactory.java | 12 + .../EngineGetPayloadBodiesResultV1.java | 79 ++++ .../ExecutionEngineJsonRpcMethods.java | 6 + .../EngineGetPayloadBodiesByHashV1Test.java | 267 +++++++++++++ .../EngineGetPayloadBodiesByRangeV1Test.java | 352 ++++++++++++++++++ 10 files changed, 919 insertions(+), 1 deletion(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV1.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1Test.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 43857f46d4..f0b930f878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ tests are updated to use EC private keys instead of RSA keys. - Improve get account performance by using the world state updater cache [#4897](https://github.com/hyperledger/besu/pull/4897) - Add new KZG precompile and option to override the trusted setup being used [#4822](https://github.com/hyperledger/besu/issues/4822) - Add implementation for eth_createAccessList RPC method [#4942](https://github.com/hyperledger/besu/pull/4942) +- Add implementation for engine_exchangeCapabilities [#4997](https://github.com/hyperledger/besu/pull/4997) +- Add implementation for engine_getPayloadBodiesByRangeV1 and engine_getPayloadBodiesByHashV1 [#4980](https://github.com/hyperledger/besu/pull/4980) - Updated reference tests to v11.3 [#4996](https://github.com/hyperledger/besu/pull/4996) - Add DebugGetRawBlock and DebugGetRawHeader RPC methods [#5011](https://github.com/hyperledger/besu/pull/5011) - Besu requires minimum Java 17 and up to build and run [#3320](https://github.com/hyperledger/besu/issues/3320) 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 cb20677899..25e1786b1d 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 @@ -56,6 +56,8 @@ public enum RpcMethod { ENGINE_FORKCHOICE_UPDATED_V1("engine_forkchoiceUpdatedV1"), ENGINE_FORKCHOICE_UPDATED_V2("engine_forkchoiceUpdatedV2"), ENGINE_EXCHANGE_TRANSITION_CONFIGURATION("engine_exchangeTransitionConfigurationV1"), + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1("engine_getPayloadBodiesByHashV1"), + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1("engine_getPayloadBodiesByRangeV1"), ENGINE_EXCHANGE_CAPABILITIES("engine_exchangeCapabilities"), GOQUORUM_ETH_GET_QUORUM_PAYLOAD("eth_getQuorumPayload"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1.java new file mode 100644 index 0000000000..4c1f451403 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1.java @@ -0,0 +1,85 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.hyperledger.besu.util.Slf4jLambdaHelper.traceLambda; + +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.methods.ExecutionEngineJsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +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.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV1; +import org.hyperledger.besu.ethereum.chain.Blockchain; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import io.vertx.core.Vertx; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EngineGetPayloadBodiesByHashV1 extends ExecutionEngineJsonRpcMethod { + + private static final int MAX_REQUEST_BLOCKS = 1024; + private static final Logger LOG = LoggerFactory.getLogger(EngineGetPayloadBodiesByHashV1.class); + private final BlockResultFactory blockResultFactory; + + public EngineGetPayloadBodiesByHashV1( + 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_V1.getMethodName(); + } + + @Override + public JsonRpcResponse syncResponse(final JsonRpcRequestContext request) { + engineCallListener.executionEngineCalled(); + + final Object reqId = request.getRequest().getId(); + + final Hash[] blockHashes = request.getRequiredParameter(0, Hash[].class); + + traceLambda(LOG, "{} parameters: blockHashes {}", () -> getName(), () -> blockHashes); + + if (blockHashes.length > getMaxRequestBlocks()) { + return new JsonRpcErrorResponse(reqId, JsonRpcError.INVALID_RANGE_REQUEST_TOO_LARGE); + } + + final Blockchain blockchain = protocolContext.getBlockchain(); + + final EngineGetPayloadBodiesResultV1 engineGetPayloadBodiesResultV1 = + blockResultFactory.payloadBodiesCompleteV1( + Arrays.stream(blockHashes).map(blockchain::getBlockBody).collect(Collectors.toList())); + + return new JsonRpcSuccessResponse(reqId, engineGetPayloadBodiesResultV1); + } + + protected int getMaxRequestBlocks() { + return MAX_REQUEST_BLOCKS; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1.java new file mode 100644 index 0000000000..a9d1a120a8 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1.java @@ -0,0 +1,113 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.hyperledger.besu.util.Slf4jLambdaHelper.traceLambda; + +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.methods.ExecutionEngineJsonRpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.UnsignedLongParameter; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +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.results.BlockResultFactory; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.EngineGetPayloadBodiesResultV1; +import org.hyperledger.besu.ethereum.chain.Blockchain; + +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 EngineGetPayloadBodiesByRangeV1 extends ExecutionEngineJsonRpcMethod { + private static final Logger LOG = LoggerFactory.getLogger(EngineGetPayloadBodiesByRangeV1.class); + private static final int MAX_REQUEST_BLOCKS = 1024; + private final BlockResultFactory blockResultFactory; + + public EngineGetPayloadBodiesByRangeV1( + 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_V1.getMethodName(); + } + + @Override + public JsonRpcResponse syncResponse(final JsonRpcRequestContext request) { + engineCallListener.executionEngineCalled(); + + final long startBlockNumber = + request.getRequiredParameter(0, UnsignedLongParameter.class).getValue(); + final long count = request.getRequiredParameter(1, UnsignedLongParameter.class).getValue(); + final Object reqId = request.getRequest().getId(); + + traceLambda( + LOG, + "{} parameters: start block number {} count {}", + () -> getName(), + () -> startBlockNumber, + () -> count); + + if (startBlockNumber < 1 || count < 1) { + return new JsonRpcErrorResponse(reqId, JsonRpcError.INVALID_PARAMS); + } + + if (count > getMaxRequestBlocks()) { + return new JsonRpcErrorResponse(reqId, JsonRpcError.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 EngineGetPayloadBodiesResultV1()); + } + + 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; + + EngineGetPayloadBodiesResultV1 engineGetPayloadBodiesResultV1 = + blockResultFactory.payloadBodiesCompleteV1( + LongStream.range(startBlockNumber, endExclusiveBlockNumber) + .mapToObj( + blockNumber -> + blockchain + .getBlockHashByNumber(blockNumber) + .flatMap(blockchain::getBlockBody)) + .collect(Collectors.toList())); + + return new JsonRpcSuccessResponse(reqId, engineGetPayloadBodiesResultV1); + } + + protected int getMaxRequestBlocks() { + return MAX_REQUEST_BLOCKS; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java index 7321375db5..8349f93afd 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/response/JsonRpcError.java @@ -83,7 +83,7 @@ public enum JsonRpcError { INVALID_TERMINAL_BLOCK(-32002, "Terminal block doesn't satisfy terminal block conditions"), INVALID_FORKCHOICE_STATE(-38002, "Invalid forkchoice state"), INVALID_PAYLOAD_ATTRIBUTES(-38003, "Invalid payload attributes"), - + INVALID_RANGE_REQUEST_TOO_LARGE(-38004, "Too large request"), // Miner failures COINBASE_NOT_SET(-32010, "Coinbase not set. Unable to start mining without a coinbase"), NO_HASHES_PER_SECOND(-32011, "No hashes being generated by the current node"), 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 1ad670d5ce..2685f888a3 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,9 +16,11 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; +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; +import org.hyperledger.besu.ethereum.core.BlockBody; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockValueCalculator; import org.hyperledger.besu.ethereum.core.BlockWithReceipts; @@ -26,6 +28,7 @@ import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; @@ -115,6 +118,15 @@ public class BlockResultFactory { Quantity.create(blockValue)); } + public EngineGetPayloadBodiesResultV1 payloadBodiesCompleteV1( + final List> blockBodies) { + final List payloadBodies = + blockBodies.stream() + .map(maybeBody -> maybeBody.map(PayloadBody::new).orElse(null)) + .collect(Collectors.toList()); + return new EngineGetPayloadBodiesResultV1(payloadBodies); + } + public BlockResult transactionHash(final BlockWithMetadata blockWithMetadata) { return transactionHash(blockWithMetadata, false); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV1.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV1.java new file mode 100644 index 0000000000..4358230ee0 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/EngineGetPayloadBodiesResultV1.java @@ -0,0 +1,79 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.TransactionEncoder; + +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; +import org.apache.tuweni.bytes.Bytes; + +@JsonPropertyOrder({"payloadBodies"}) +public class EngineGetPayloadBodiesResultV1 { + + private final List payloadBodies; + + public EngineGetPayloadBodiesResultV1() { + this.payloadBodies = Collections.emptyList(); + } + + public EngineGetPayloadBodiesResultV1(final List payloadBody) { + this.payloadBodies = payloadBody; + } + + @JsonValue + public List getPayloadBodies() { + return payloadBodies; + } + + public static class PayloadBody { + private final List transactions; + private final List withdrawals; + + public PayloadBody(final BlockBody blockBody) { + this.transactions = + blockBody.getTransactions().stream() + .map(TransactionEncoder::encodeOpaqueBytes) + .map(Bytes::toHexString) + .collect(Collectors.toList()); + this.withdrawals = + blockBody + .getWithdrawals() + .map( + ws -> + ws.stream() + .map(WithdrawalParameter::fromWithdrawal) + .collect(Collectors.toList())) + .orElse(null); + } + + @JsonGetter(value = "transactions") + public List getTransactions() { + return transactions; + } + + @JsonGetter(value = "withdrawals") + public List getWithdrawals() { + return withdrawals; + } + } +} 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 0de76d5b62..ccf5033478 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 @@ -22,6 +22,8 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineE import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineExchangeTransitionConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineForkchoiceUpdatedV1; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineForkchoiceUpdatedV2; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByHashV1; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.engine.EngineGetPayloadBodiesByRangeV1; 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.EngineNewPayloadV1; @@ -114,6 +116,10 @@ public class ExecutionEngineJsonRpcMethods extends ApiGroupJsonRpcMethods { engineQosTimer), new EngineExchangeTransitionConfiguration( consensusEngineServer, protocolContext, engineQosTimer), + new EngineGetPayloadBodiesByHashV1( + consensusEngineServer, protocolContext, blockResultFactory, engineQosTimer), + new EngineGetPayloadBodiesByRangeV1( + consensusEngineServer, protocolContext, blockResultFactory, engineQosTimer), new EngineExchangeCapabilities(consensusEngineServer, protocolContext, engineQosTimer)); } else { return mapOf( diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1Test.java new file mode 100644 index 0000000000..03493d7f05 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByHashV1Test.java @@ -0,0 +1,267 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.response.JsonRpcError.INVALID_RANGE_REQUEST_TOO_LARGE; +import static org.mockito.Mockito.doReturn; +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.GWei; +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.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +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.JsonRpcResponseType; +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.EngineGetPayloadBodiesResultV1; +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.core.Withdrawal; + +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.UInt64; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EngineGetPayloadBodiesByHashV1Test { + private EngineGetPayloadBodiesByHashV1 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; + + @Before + public void before() { + when(protocolContext.getBlockchain()).thenReturn(blockchain); + this.method = + spy( + new EngineGetPayloadBodiesByHashV1( + vertx, protocolContext, blockResultFactory, engineCallListener)); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("engine_getPayloadBodiesByHashV1"); + } + + @Test + public void shouldReturnEmptyPayloadBodiesWithEmptyHash() { + final var resp = resp(new Hash[] {}); + final EngineGetPayloadBodiesResultV1 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().isEmpty()).isTrue(); + } + + @Test + public void shouldReturnPayloadForKnownHashes() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = 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()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockBody blockBody3 = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody1)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(blockBody2)); + when(blockchain.getBlockBody(blockHash3)).thenReturn(Optional.of(blockBody3)); + + 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).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(2).getTransactions().size()).isEqualTo(3); + } + + @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 shouldReturnNullForUnknownHashAndPayloadForKnownHash() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = Hash.wrap(Bytes32.random()); + final BlockBody blockBody1 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockBody blockBody3 = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody1)); + when(blockchain.getBlockBody(blockHash3)).thenReturn(Optional.of(blockBody3)); + + 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).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1)).isNull(); + assertThat(result.getPayloadBodies().get(2).getTransactions().size()).isEqualTo(3); + } + + @Test + public void shouldReturnWithdrawalNullWhenBlockIsPreShanghai() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final BlockBody preShanghaiBlockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + + final BlockBody preShanghaiBlockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.empty()); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(preShanghaiBlockBody)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(preShanghaiBlockBody2)); + + final var resp = resp(new Hash[] {blockHash1, blockHash2}); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getWithdrawals()).isNull(); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getWithdrawals()).isNull(); + } + + @Test + public void shouldReturnWithdrawalsWhenBlockIsPostShanghai() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Withdrawal withdrawal = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); + final Withdrawal withdrawal2 = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x2"), GWei.ONE); + + final BlockBody shanghaiBlockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + + final BlockBody shanghaiBlockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal2))); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(shanghaiBlockBody)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(shanghaiBlockBody2)); + + final var resp = resp(new Hash[] {blockHash1, blockHash2}); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getWithdrawals().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getWithdrawals().size()).isEqualTo(1); + } + + @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_V1.getMethodName(), + new Object[] {hashes}))); + } + + private EngineGetPayloadBodiesResultV1 fromSuccessResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + return Optional.of(resp) + .map(JsonRpcSuccessResponse.class::cast) + .map(JsonRpcSuccessResponse::getResult) + .map(EngineGetPayloadBodiesResultV1.class::cast) + .get(); + } + + private JsonRpcError fromErrorResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(JsonRpcResponseType.ERROR); + return Optional.of(resp) + .map(JsonRpcErrorResponse.class::cast) + .map(JsonRpcErrorResponse::getError) + .get(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1Test.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1Test.java new file mode 100644 index 0000000000..5711d05d27 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/engine/EngineGetPayloadBodiesByRangeV1Test.java @@ -0,0 +1,352 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.response.JsonRpcError.INVALID_PARAMS; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError.INVALID_RANGE_REQUEST_TOO_LARGE; +import static org.mockito.Mockito.doReturn; +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.GWei; +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.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +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.JsonRpcResponseType; +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.EngineGetPayloadBodiesResultV1; +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.core.Withdrawal; + +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.UInt64; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EngineGetPayloadBodiesByRangeV1Test { + private EngineGetPayloadBodiesByRangeV1 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; + + @Before + public void before() { + when(protocolContext.getBlockchain()).thenReturn(blockchain); + this.method = + spy( + new EngineGetPayloadBodiesByRangeV1( + vertx, protocolContext, blockResultFactory, engineCallListener)); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("engine_getPayloadBodiesByRangeV1"); + } + + @Test + public void shouldReturnPayloadForKnownNumber() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = 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()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockBody blockBody3 = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody1)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(blockBody2)); + when(blockchain.getBlockBody(blockHash3)).thenReturn(Optional.of(blockBody3)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(124)).thenReturn(Optional.of(blockHash2)); + when(blockchain.getBlockHashByNumber(125)).thenReturn(Optional.of(blockHash3)); + + final var resp = resp("0x7b", "0x3"); + final EngineGetPayloadBodiesResultV1 result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(2).getTransactions().size()).isEqualTo(3); + } + + @Test + public void shouldReturnNullForUnknownNumber() { + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + final var resp = resp("0x7b", "0x3"); + final EngineGetPayloadBodiesResultV1 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 shouldReturnNullForUnknownNumberAndPayloadForKnownNumber() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = Hash.wrap(Bytes32.random()); + final BlockBody blockBody1 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + final BlockBody blockBody3 = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(blockBody1)); + when(blockchain.getBlockBody(blockHash3)).thenReturn(Optional.of(blockBody3)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(125)).thenReturn(Optional.of(blockHash3)); + + final var resp = resp("0x7b", "0x3"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1)).isNull(); + assertThat(result.getPayloadBodies().get(2).getTransactions().size()).isEqualTo(3); + } + + @Test + public void shouldReturnNullForWithdrawalsWhenBlockIsPreShanghai() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + + final BlockBody preShanghaiBlockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList()); + + final BlockBody preShanghaiBlockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.empty()); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(preShanghaiBlockBody)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(preShanghaiBlockBody2)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(124)).thenReturn(Optional.of(blockHash2)); + + final var resp = resp("0x7b", "0x2"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getWithdrawals()).isNull(); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getWithdrawals()).isNull(); + ; + } + + @Test + public void shouldReturnWithdrawalsWhenBlockIsPostShanghai() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Withdrawal withdrawal = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); + final Withdrawal withdrawal2 = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x2"), GWei.ONE); + + final BlockBody shanghaiBlockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + + final BlockBody shanghaiBlockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal2))); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(130)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(shanghaiBlockBody)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(shanghaiBlockBody2)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(124)).thenReturn(Optional.of(blockHash2)); + + final var resp = resp("0x7b", "0x2"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(2); + assertThat(result.getPayloadBodies().get(0).getTransactions().size()).isEqualTo(3); + assertThat(result.getPayloadBodies().get(0).getWithdrawals().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getTransactions().size()).isEqualTo(1); + assertThat(result.getPayloadBodies().get(1).getWithdrawals().size()).isEqualTo(1); + } + + @Test + public void shouldNotContainTrailingNullForBlocksPastTheCurrentHead() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Withdrawal withdrawal = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); + + final BlockBody shanghaiBlockBody = + new BlockBody( + List.of( + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair()), + new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(123)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(shanghaiBlockBody)); + 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 shouldReturnUpUntilHeadWhenStartBlockPlusCountEqualsHeadNumber() { + final SignatureAlgorithm sig = SignatureAlgorithmFactory.getInstance(); + final Hash blockHash1 = Hash.wrap(Bytes32.random()); + final Hash blockHash2 = Hash.wrap(Bytes32.random()); + final Hash blockHash3 = Hash.wrap(Bytes32.random()); + final Withdrawal withdrawal = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); + + final BlockBody shanghaiBlockBody = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + final BlockBody shanghaiBlockBody2 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + final BlockBody shanghaiBlockBody3 = + new BlockBody( + List.of(new TransactionTestFixture().createTransaction(sig.generateKeyPair())), + Collections.emptyList(), + Optional.of(List.of(withdrawal))); + when(blockchain.getChainHeadBlockNumber()).thenReturn(Long.valueOf(125)); + when(blockchain.getBlockBody(blockHash1)).thenReturn(Optional.of(shanghaiBlockBody)); + when(blockchain.getBlockBody(blockHash2)).thenReturn(Optional.of(shanghaiBlockBody2)); + when(blockchain.getBlockBody(blockHash3)).thenReturn(Optional.of(shanghaiBlockBody3)); + when(blockchain.getBlockHashByNumber(123)).thenReturn(Optional.of(blockHash1)); + when(blockchain.getBlockHashByNumber(124)).thenReturn(Optional.of(blockHash2)); + when(blockchain.getBlockHashByNumber(125)).thenReturn(Optional.of(blockHash3)); + + final var resp = resp("0x7b", "0x3"); + final var result = fromSuccessResp(resp); + assertThat(result.getPayloadBodies().size()).isEqualTo(3); + } + + @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_V1.getMethodName(), + new Object[] {startBlockNumber, range}))); + } + + private EngineGetPayloadBodiesResultV1 fromSuccessResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + return Optional.of(resp) + .map(JsonRpcSuccessResponse.class::cast) + .map(JsonRpcSuccessResponse::getResult) + .map(EngineGetPayloadBodiesResultV1.class::cast) + .get(); + } + + private JsonRpcError fromErrorResp(final JsonRpcResponse resp) { + assertThat(resp.getType()).isEqualTo(JsonRpcResponseType.ERROR); + return Optional.of(resp) + .map(JsonRpcErrorResponse.class::cast) + .map(JsonRpcErrorResponse::getError) + .get(); + } +}