diff --git a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcRequestHandler.java b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcRequestHandler.java index 1e4a809f5ec..11366b4fe56 100644 --- a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcRequestHandler.java +++ b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcRequestHandler.java @@ -44,7 +44,7 @@ public byte[] handleRequest(byte[] body) throws IOException { request = OBJECT_MAPPER.reader() .with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) .with(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS).readTree(body); - } catch (IOException e) { + } catch (IOException | NumberFormatException e) { return OBJECT_MAPPER.writeValueAsBytes(errorResponse(JsonError.PARSE_ERROR, null)); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcRequestValidator.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcRequestValidator.java index f7eada9fe53..f518f440d7d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcRequestValidator.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcRequestValidator.java @@ -6,6 +6,8 @@ public final class JsonRpcRequestValidator { private static final String INVALID_REQUEST = "Invalid Request"; + public static final String INVALID_NUMERIC_ID_MESSAGE = + INVALID_REQUEST + ": numeric id must be an integer literal within the signed 64-bit range"; private JsonRpcRequestValidator() { } @@ -15,6 +17,9 @@ public static String invalidRequestMessage(JsonNode request) { if (request != null && request.isObject() && hasInvalidVersion(request)) { return INVALID_REQUEST + ": jsonrpc must be \"2.0\""; } + if (request != null && request.isObject() && hasInvalidNumericId(request)) { + return INVALID_NUMERIC_ID_MESSAGE; + } // An explicit null ID is invalid: jsonrpc4j treats it as a notification and drops the result. // Explicit null params retain the public endpoint's existing dispatch behavior. if (request == null || !request.isObject() @@ -31,6 +36,12 @@ public static boolean isValidResponseId(JsonNode id) { return id != null && (id.isTextual() || id.isNumber()); } + /** jsonrpc4j parses IDs beyond the long range with asInt() and fractional IDs with asDouble(). */ + private static boolean hasInvalidNumericId(JsonNode request) { + JsonNode id = request.get("id"); + return id != null && id.isNumber() && !(id.isIntegralNumber() && id.canConvertToLong()); + } + private static boolean hasInvalidVersion(JsonNode request) { JsonNode version = request.get("jsonrpc"); return version == null || !version.isTextual() || !"2.0".equals(version.asText()); diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/server/AdminJsonRpcRequestTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/server/AdminJsonRpcRequestTest.java index b45be6001e8..88f2addd6a9 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/server/AdminJsonRpcRequestTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/server/AdminJsonRpcRequestTest.java @@ -32,6 +32,7 @@ import org.tron.core.services.admin.http.AdminRpcServlet; import org.tron.core.services.filter.HttpInterceptor; import org.tron.core.services.jsonrpc.JsonRpcMediaType; +import org.tron.core.services.jsonrpc.JsonRpcRequestValidator; /** Runs identical protocol assertions through the HTTP and IPC entry points. */ @RunWith(Parameterized.class) @@ -100,10 +101,31 @@ public void invalidIdTypesAreRejectedBeforeInvocation() throws Exception { } @Test - public void numericIdsOutsideLongRangeRetainTheirValue() throws Exception { + public void nonLongNumericIdsAreRejectedWithTheirValue() throws Exception { for (String id : new String[] {"9223372036854775808", "-9223372036854775809", - "18446744073709551617", "9223372036854775807.1", "-9223372036854775808.1", - "1e40", "-1e40"}) { + "18446744073709551617", "9223372036854775807.1", "-9223372036854775808.1", "1.5", + "9223372036854775807.0", "0.12345678901234567890123456789", "-0.5", "1e-20", "1e40", + "-1e40"}) { + JsonNode response = assertError(sender.send(REQUEST + ",\"id\":" + id + "}"), -32600, id); + Assert.assertEquals(JsonRpcRequestValidator.INVALID_NUMERIC_ID_MESSAGE, + response.path("error").path("message").asText()); + } + Mockito.verifyNoInteractions(adminJsonRpc); + } + + @Test + public void exponentOverflowIsAParseError() throws Exception { + for (String literal : new String[] {"1e2147483648", "-1e2147483648", "1e-2147483649"}) { + assertError(sender.send(REQUEST + ",\"id\":" + literal + "}"), -32700, "null"); + assertError(sender.send("{\"jsonrpc\":\"2.0\",\"method\":\"admin_addPeer\"," + + "\"params\":[" + literal + "],\"id\":1}"), -32700, "null"); + } + Mockito.verifyNoInteractions(adminJsonRpc); + } + + @Test + public void longRangeIdsRetainTheirValue() throws Exception { + for (String id : new String[] {"9223372036854775807", "-9223372036854775808"}) { JsonNode response = parse(sender.send(REQUEST + ",\"id\":" + id + "}")); assertId(id, response.get("id")); Assert.assertTrue(response.path("result").path("success").asBoolean()); @@ -115,8 +137,7 @@ public void numericIdsOutsideLongRangeRetainTheirValue() throws Exception { @Test public void acceptedIdsRetainTheirValueAndType() throws Exception { for (String id : new String[] {"\"null\"", "\"\"", "\"请求-7\"", "7", "0", - "9223372036854775807", "-9223372036854775808", "9007199254740993", - "0.12345678901234567890123456789", "9223372036854775807.0", "-0.5", "1e-20"}) { + "9223372036854775807", "-9223372036854775808", "9007199254740993"}) { JsonNode response = parse(sender.send(REQUEST + ",\"id\":" + id + "}")); assertId(id, response.get("id")); Assert.assertTrue(response.path("result").path("success").asBoolean()); @@ -230,8 +251,7 @@ public void invalidMethodParamsNotificationHasNoResponse() throws Exception { public void parameterBindingFailuresRetainIdsAndSilenceNotifications() throws Exception { for (String params : new String[] {"[{}]", "{\"endpoint\":{}}"}) { String request = REQUEST.replace(PARAMS, params); - for (String id : new String[] {"9", "\"request\"", "9223372036854775807", - "0.12345678901234567890123456789"}) { + for (String id : new String[] {"9", "\"request\"", "9223372036854775807"}) { JsonNode error = assertError(sender.send(request + ",\"id\":" + id + "}"), -32602, id) .get("error"); Assert.assertEquals("Failed to read method parameter at index 0", @@ -255,8 +275,7 @@ public void businessErrorNotificationHasNoResponse() throws Exception { public void errorResponsesRetainAcceptedIds() throws Exception { Mockito.when(adminJsonRpc.addPeer(ENDPOINT)) .thenThrow(new JsonRpcInvalidParamsException("Invalid admin parameters")); - for (String id : new String[] {"\"null\"", "9223372036854775807", - "0.12345678901234567890123456789"}) { + for (String id : new String[] {"\"null\"", "9223372036854775807"}) { JsonNode response = assertError(sender.send(REQUEST + ",\"id\":" + id + "}"), -32602, id); Assert.assertEquals("Invalid admin parameters", response.path("error").path("message") .asText()); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index 5c356db5494..f3db654e776 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -1292,6 +1292,90 @@ public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exce } } + // --- Numeric ids that are not long integers --- + + @Test + public void invalidNumericId_isRejectedWithOriginalIdWithoutDispatch() throws Exception { + for (String id : new String[] {"9223372036854775808", "-9223372036854775809", + "18446744073709551617", "1.5", "9223372036854775807.1", "1e19", "-1e3"}) { + MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\"," + + "\"method\":\"web3_clientVersion\",\"params\":[],\"id\":" + id + "}"); + assertEquals(200, resp.getStatus()); + assertInvalidRequest(MAPPER.readTree(resp.getContentAsByteArray()), + JsonRpcRequestValidator.INVALID_NUMERIC_ID_MESSAGE, id); + } + + verifyNoInteractions(mockRpcServer); + } + + @Test + public void longRangeIds_areServedWhileLargerIdIsRejectedInBatch() throws Exception { + TronJsonRpc rpc = useRealRpcServer(); + + MockHttpServletResponse resp = doPost("[" + + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":9223372036854775807}," + + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":9223372036854775808}," + + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":-9223372036854775808}]"); + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertEquals(3, body.size()); + assertEquals(Long.MAX_VALUE, body.get(0).get("id").asLong()); + assertEquals("0x1", body.get(0).get("result").asText()); + assertInvalidRequest(body.get(1), JsonRpcRequestValidator.INVALID_NUMERIC_ID_MESSAGE, + "9223372036854775808"); + assertEquals(Long.MIN_VALUE, body.get(2).get("id").asLong()); + assertEquals("0x1", body.get(2).get("result").asText()); + verify(rpc, times(2)).getLatestBlockNum(); + } + + @Test + public void rejectedFractionalId_isEchoedAsParsedDouble() throws Exception { + for (String id : new String[] {"0.12345678901234567890123456789", "1e-20", "1e300"}) { + MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\"," + + "\"method\":\"web3_clientVersion\",\"params\":[],\"id\":" + id + "}"); + assertInvalidRequest(MAPPER.readTree(resp.getContentAsByteArray()), + JsonRpcRequestValidator.INVALID_NUMERIC_ID_MESSAGE, + Double.toString(Double.parseDouble(id))); + } + + verifyNoInteractions(mockRpcServer); + } + + @Test + public void exponentOverflowId_isRejected() throws Exception { + for (String id : new String[] {"1e400", "-1e400", "1e2147483648"}) { + MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\"," + + "\"method\":\"web3_clientVersion\",\"params\":[],\"id\":" + id + "}"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals(JsonRpcRequestValidator.INVALID_NUMERIC_ID_MESSAGE, + body.get("error").get("message").asText()); + // Jackson writes the parsed double, which has no JSON number form, as a string. + assertEquals(Double.toString(Double.parseDouble(id)), body.get("id").asText()); + } + + verifyNoInteractions(mockRpcServer); + } + + @Test + public void nonBooleanFloatParam_isRejectedInSingleAndBatch() throws Exception { + TronJsonRpc rpc = useRealRpcServer(); + + for (String literal : new String[] {"1.0", "0.0", "1e0", "0e0", "1e2147483648"}) { + String request = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByNumber\"," + + "\"params\":[\"latest\"," + literal + "],\"id\":1}"; + JsonNode single = MAPPER.readTree(doPost(request).getContentAsByteArray()); + JsonNode batch = MAPPER.readTree(doPost("[" + request + "]").getContentAsByteArray()); + assertEquals(literal, 1, batch.size()); + for (JsonNode response : new JsonNode[] {single, batch.get(0)}) { + assertEquals(literal, 1, response.get("id").asInt()); + assertEquals(literal, -32602, response.get("error").get("code").asInt()); + } + } + + verify(rpc, never()).ethGetBlockByNumber(any(), any()); + } + // --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced --- @Test