diff --git a/CHANGELOG.md b/CHANGELOG.md index 209469fbe55..c5a0980e8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ ### Breaking Changes - Besu now requires JDK 25 to build and run. - RPC changes to enhance compatibility with other ELs + - Block number parameter in RPCs now only supports hex values. Non-hex (decimal) block number parameters are now rejected. Affected RPCs including but not limited to: `admin_logsRemoveCache`, `admin_generateLogBloomCache`, `eth_estimateGas`, `eth_getBlockByNumber`, `eth_getBlockTransactionCountByNumber`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getUncleByBlockNumberAndIndex`, `eth_getUncleCountByBlockNumber`, `eth_feeHistory`, `eth_getProof`, `trace_block`, `trace_call`, `trace_callMany`, `trace_replayBlockTransactions`, `debug_traceBlockByNumber`, `debug_traceCall`, `debug_replayBlock`, `debug_getRawBlock`, `debug_getRawHeader`, and `debug_getRawReceipts` [#10515](https://github.com/besu-eth/besu/pull/10515), [#10240](https://github.com/besu-eth/besu/pull/10240) - Hash parameter in RPCs now only supports hex values. Hash values without a `0x` prefix are now rejected with `-32602 INVALID_PARAMS`. Affected RPCs including but not limited to: `debug_getRawTransaction`, `eth_getTransactionByHash`, `eth_getTransactionReceipt` [#10505](https://github.com/besu-eth/besu/pull/10505) - - Block number parameter in RPCs now only supports hex values. Non-hex (decimal) block number parameters are now rejected. Affected RPCs including but not limited to: `admin_logsRemoveCache`, `admin_generateLogBloomCache`, `eth_estimateGas`, `eth_getBlockByNumber`, `eth_getBlockTransactionCountByNumber`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getUncleByBlockNumberAndIndex`, `eth_getUncleCountByBlockNumber`, `eth_feeHistory`, `trace_block`, `trace_call`, `trace_callMany`, `trace_replayBlockTransactions`, `debug_traceBlockByNumber`, `debug_traceCall`, and `debug_replayBlock` [#10515](https://github.com/besu-eth/besu/pull/10515), [#10240](https://github.com/besu-eth/besu/pull/10240) ### Upcoming Breaking Changes - Sunsetting features - for more context on the reasoning behind the deprecation of these features, including alternative options, read [this blog post](https://www.lfdecentralizedtrust.org/blog/sunsetting-tessera-and-simplifying-hyperledger-besu) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceipts.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceipts.java index ca7f527aa35..c7969fdd7d6 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceipts.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceipts.java @@ -14,11 +14,10 @@ */ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; -import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; @@ -31,7 +30,7 @@ import com.google.common.base.Suppliers; -public class DebugGetRawReceipts extends AbstractBlockParameterOrBlockHashMethod { +public class DebugGetRawReceipts extends AbstractBlockParameterMethod { public DebugGetRawReceipts(final BlockchainQueries blockchain) { super(Suppliers.ofInstance(blockchain)); @@ -43,24 +42,23 @@ public String getName() { } @Override - protected BlockParameterOrBlockHash blockParameterOrBlockHash( - final JsonRpcRequestContext request) { + protected BlockParameter blockParameter(final JsonRpcRequestContext request) { try { - return request.getRequiredParameter(0, BlockParameterOrBlockHash.class); + return request.getRequiredParameter(0, BlockParameter.class); } catch (JsonRpcParameterException e) { throw new InvalidJsonRpcParameters( - "Invalid block or block hash parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); + "Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e); } } @Override - protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) { - return blockchainQueries - .get() - .getBlockchain() - .getTxReceipts(blockHash) + protected Object resultByBlockNumber( + final JsonRpcRequestContext request, final long blockNumber) { + return getBlockchainQueries() + .getBlockHashByNumber(blockNumber) + .flatMap(blockHash -> getBlockchainQueries().getBlockchain().getTxReceipts(blockHash)) .map(this::toRLP) - .orElseGet(() -> new String[0]); + .orElse(null); } private String[] toRLP(final List receipts) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHash.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHash.java index 6cc537e6922..0e767442cb3 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHash.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHash.java @@ -79,6 +79,9 @@ public BlockParameterOrBlockHash(final Object value) throws JsonProcessingExcept requireCanonical = false; } else if (normalizedValue.length() > 16) { throw new IllegalArgumentException("hex number > 64 bits"); + } else if (!normalizedValue.startsWith("0x")) { + throw new IllegalArgumentException( + "Invalid block number: must be a hex string with 0x prefix"); } else { type = BlockParameterType.NUMERIC; number = OptionalLong.of(Long.decode(value.toString())); @@ -97,8 +100,13 @@ public BlockParameterOrBlockHash(final Object value) throws JsonProcessingExcept requireCanonical = false; } } else { + final String blockNumberText = jsonNode.get("blockNumber").asText(); + if (!blockNumberText.toLowerCase(Locale.ROOT).startsWith("0x")) { + throw new IllegalArgumentException( + "Invalid block number: must be a hex string with 0x prefix"); + } type = BlockParameterType.NUMERIC; - number = OptionalLong.of(Long.decode(jsonNode.get("blockNumber").asText())); + number = OptionalLong.of(Long.decode(blockNumberText)); blockHash = Optional.empty(); requireCanonical = false; } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTest.java index 518fbb558a5..81acb4b6189 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTest.java @@ -1994,9 +1994,7 @@ public void ethGetStorageAtBlockNumber() throws Exception { + address + "\",\"" + UInt256.ZERO - + "\",\"" - + 0L - + "\"]}", + + "\",\"0x0\"]}", JSON); try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceiptsTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceiptsTest.java new file mode 100644 index 00000000000..b6f7390273f --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugGetRawReceiptsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Hash; +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.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; +import org.hyperledger.besu.ethereum.chain.Blockchain; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DebugGetRawReceiptsTest { + + private BlockchainQueries blockchainQueries; + private Blockchain blockchain; + private DebugGetRawReceipts method; + + @BeforeEach + public void setUp() { + blockchainQueries = mock(BlockchainQueries.class); + blockchain = mock(Blockchain.class); + when(blockchainQueries.getBlockchain()).thenReturn(blockchain); + method = new DebugGetRawReceipts(blockchainQueries); + } + + @Test + public void returnsNullForMissingBlock() { + final long missingBlockNumber = 999_999_999L; + when(blockchainQueries.getBlockHashByNumber(missingBlockNumber)).thenReturn(Optional.empty()); + + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + "debug_getRawReceipts", + new Object[] {"0x" + Long.toHexString(missingBlockNumber)})); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + assertThat(response.getResult()).isNull(); + } + + @Test + public void returnsNullForFutureBlock() { + final long futureBlockNumber = Long.MAX_VALUE; + when(blockchainQueries.getBlockHashByNumber(futureBlockNumber)).thenReturn(Optional.empty()); + + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + "debug_getRawReceipts", + new Object[] {"0x" + Long.toHexString(futureBlockNumber)})); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + assertThat(response.getResult()).isNull(); + } + + @Test + public void returnsEmptyArrayForBlockWithNoReceipts() { + final long blockNumber = 42L; + final Hash blockHash = Hash.fromHexStringLenient("0x1234"); + when(blockchainQueries.getBlockHashByNumber(blockNumber)).thenReturn(Optional.of(blockHash)); + when(blockchain.getTxReceipts(blockHash)).thenReturn(Optional.of(java.util.List.of())); + + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest( + "2.0", + "debug_getRawReceipts", + new Object[] {"0x" + Long.toHexString(blockNumber)})); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + assertThat((String[]) response.getResult()).isEmpty(); + } +} diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java index 7a0ebd2ac01..5238955bc69 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugSetHeadTest.java @@ -152,7 +152,8 @@ public void assertNullWhenBlockNotFound() { // move the head to number just after chain head var resp = - debugSetHead.response(debugSetHead("" + chainTip.getNumber() + 1, Optional.of(TRUE))); + debugSetHead.response( + debugSetHead("0x" + Long.toHexString(chainTip.getNumber() + 1), Optional.of(TRUE))); // success with null result if block not found assertThat(resp.getType()).isEqualTo(RpcResponseType.SUCCESS); assertThat(((JsonRpcSuccessResponse) resp).getResult()).isNull(); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java index d3adb5f1c78..7c5d2014125 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java @@ -124,7 +124,7 @@ void shouldReturnNullWhenWorldStateUnavailable() { requestWithParams( Address.fromHexString("0x0000000000000000000000000000000000000000"), new String[] {storageKey.toString()}, - String.valueOf(501)); + "0x" + Long.toHexString(blockNumber + 1)); final JsonRpcResponse response = method.response(request); @@ -139,7 +139,9 @@ void getProofWithAccount() { final JsonRpcRequestContext request = requestWithParams( - address.toString(), new String[] {storageKey.toString()}, String.valueOf(blockNumber)); + address.toString(), + new String[] {storageKey.toString()}, + "0x" + Long.toHexString(blockNumber)); final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); final GetProofResult result = (GetProofResult) response.getResult(); @@ -225,7 +227,9 @@ void getProofWithoutAccount() { final JsonRpcRequestContext request = requestWithParams( - address.toString(), new String[] {storageKey.toString()}, String.valueOf(blockNumber)); + address.toString(), + new String[] {storageKey.toString()}, + "0x" + Long.toHexString(blockNumber)); final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); final GetProofResult result = (GetProofResult) response.getResult(); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHashTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHashTest.java new file mode 100644 index 00000000000..759548bda23 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/BlockParameterOrBlockHashTest.java @@ -0,0 +1,53 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; + +public class BlockParameterOrBlockHashTest { + + @Test + public void hexBlockNumberStringIsAccepted() throws JsonProcessingException { + final BlockParameterOrBlockHash param = new BlockParameterOrBlockHash("0x64"); + assertThat(param.getNumber()).hasValue(100); + } + + @Test + public void decimalBlockNumberStringIsRejected() { + assertThatThrownBy(() -> new BlockParameterOrBlockHash("100")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("0x prefix"); + } + + @Test + public void hexBlockNumberInEip1898ObjectFormIsAccepted() throws JsonProcessingException { + // EIP-1898 object form: {"blockNumber": "0x64"} + final BlockParameterOrBlockHash param = + new BlockParameterOrBlockHash(java.util.Map.of("blockNumber", "0x64")); + assertThat(param.getNumber()).hasValue(100); + } + + @Test + public void decimalBlockNumberInEip1898ObjectFormIsRejected() { + // EIP-1898 object form with decimal: {"blockNumber": "100"} must be rejected + assertThatThrownBy(() -> new BlockParameterOrBlockHash(java.util.Map.of("blockNumber", "100"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("0x prefix"); + } +} diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getBalance_illegalRangeLessThan.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getBalance_illegalRangeLessThan.json index 2a59465e88f..3447b185ecb 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getBalance_illegalRangeLessThan.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getBalance_illegalRangeLessThan.json @@ -13,7 +13,7 @@ "id": 28, "error": { "code": -32602, - "message": "Invalid block number params" + "message": "Invalid block param (block not found)" } }, "statusCode": 200 diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getCode_illegalRangeLessThan.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getCode_illegalRangeLessThan.json index e2011bb4d05..28d1b81b2f5 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getCode_illegalRangeLessThan.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getCode_illegalRangeLessThan.json @@ -13,7 +13,7 @@ "id": 13, "error": { "code": -32602, - "message": "Invalid block number params" + "message": "Invalid block param (block not found)" } }, "statusCode": 200 diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getProof_illegalRangeLessThan.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getProof_illegalRangeLessThan.json index f0cf31f79dd..1f2c912df91 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getProof_illegalRangeLessThan.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getProof_illegalRangeLessThan.json @@ -14,7 +14,7 @@ "id": 28, "error": { "code": -32602, - "message": "Invalid block number params" + "message": "Invalid block param (block not found)" } }, "statusCode": 200 diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getStorageAt_illegalRangeLessThan.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getStorageAt_illegalRangeLessThan.json index 1f46003c8f6..a6c7cec29fd 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getStorageAt_illegalRangeLessThan.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_getStorageAt_illegalRangeLessThan.json @@ -14,7 +14,7 @@ "id": 337, "error": { "code": -32602, - "message": "Invalid block number params" + "message": "Invalid block param (block not found)" } }, "statusCode": 200