From b32fa5887f89d314e5c0bd787ee9195723436369 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 18 Mar 2026 22:46:15 +0800 Subject: [PATCH 01/13] restrict batch size and response size of jsonrpc --- .../common/parameter/CommonParameter.java | 6 ++ .../org/tron/core/config/args/NodeConfig.java | 2 + common/src/main/resources/reference.conf | 6 ++ .../java/org/tron/core/config/args/Args.java | 2 + .../filter/BufferedResponseWrapper.java | 61 ++++++++++++++++ .../filter/CachedBodyRequestWrapper.java | 50 ++++++++++++++ .../core/services/jsonrpc/JsonRpcServlet.java | 69 +++++++++++++++++-- framework/src/main/resources/config.conf | 4 ++ 8 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java create mode 100644 framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index a73158a718a..29f2b909d66 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -459,6 +459,12 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxBlockFilterNum = 50000; + @Getter + @Setter + public int jsonRpcMaxBatchSize = 1; + @Getter + @Setter + public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index c3305e976de..c28b37e871c 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -302,6 +302,8 @@ public void setHttpPBFTPort(int v) { private int maxBlockRange = 5000; private int maxSubTopics = 1000; private int maxBlockFilterNum = 50000; + private int maxBatchSize = 100; + private int maxResponseSize = 25 * 1024 * 1024; } @Getter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 11970a0a673..ed5ef5684b3 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -400,6 +400,12 @@ node { # Maximum number for blockFilter maxBlockFilterNum = 50000 + + # Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit + maxBatchSize = 100 + + # Maximum response body size in bytes for JSON-RPC (default 25MB) + maxResponseSize = 26214400 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index f91c6a437ac..f13ca7f7303 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -585,6 +585,8 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange(); PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics(); PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); + PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); + PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java new file mode 100644 index 00000000000..2e4d8eb0ef2 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -0,0 +1,61 @@ +package org.tron.core.services.filter; + +import java.io.ByteArrayOutputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * Buffers the response body without writing to the underlying response, + * so the caller can inspect the size before committing. + */ +public class BufferedResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final ServletOutputStream outputStream = new ServletOutputStream() { + @Override + public void write(int b) { + buffer.write(b); + } + + @Override + public void write(byte[] b, int off, int len) { + buffer.write(b, off, len); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + }; + + public BufferedResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public ServletOutputStream getOutputStream() { + return outputStream; + } + + /** + * Suppress forwarding Content-Length to the real response; caller sets it after size check. + */ + @Override + public void setContentLength(int len) { + } + + @Override + public void setContentLengthLong(long len) { + } + + public byte[] toByteArray() { + return buffer.toByteArray(); + } + +} diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java new file mode 100644 index 00000000000..efee8d9574e --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java @@ -0,0 +1,50 @@ +package org.tron.core.services.filter; + +import java.io.ByteArrayInputStream; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** + * Wraps a request and replays a pre-read body from a byte array. + */ +public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) { + super(request); + this.body = body; + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public int read(byte[] b, int off, int len) { + return bais.read(b, off, len); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 104a0e9e470..fb26c400a24 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,10 +1,15 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; import com.googlecode.jsonrpc4j.ProxyUtil; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Collections; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -14,15 +19,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; -import org.tron.core.Wallet; -import org.tron.core.db.Manager; -import org.tron.core.services.NodeInfoService; +import org.tron.core.services.filter.BufferedResponseWrapper; +import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @Component @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private JsonRpcServer rpcServer = null; @Autowired @@ -66,6 +72,59 @@ public Integer getJsonRpcCode(int httpStatusCode) { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - rpcServer.handle(req, resp); + CommonParameter parameter = CommonParameter.getInstance(); + + // Read request body so we can inspect and replay it + byte[] body = readBody(req.getInputStream()); + + // Check batch request array length + JsonNode rootNode = MAPPER.readTree(body); + if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { + writeJsonRpcError(resp, + "Batch size " + rootNode.size() + " exceeds the limit of " + + parameter.getJsonRpcMaxBatchSize(), null); + return; + } + + // Buffer the response to check its size before committing + BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp); + rpcServer.handle(new CachedBodyRequestWrapper(req, body), bufferedResp); + + byte[] responseBytes = bufferedResp.toByteArray(); + logger.info("responseBytes: {}", responseBytes.length); + if (responseBytes.length > parameter.getJsonRpcMaxResponseSize()) { + JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; + writeJsonRpcError(resp, + "Response byte size " + responseBytes.length + " exceeds the limit of " + + parameter.getJsonRpcMaxResponseSize(), idNode); + return; + } + + resp.setContentLength(responseBytes.length); + resp.getOutputStream().write(responseBytes); + resp.getOutputStream().flush(); + } + + private byte[] readBody(InputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] tmp = new byte[4096]; + int n; + while ((n = in.read(tmp)) != -1) { + buffer.write(tmp, 0, n); + } + return buffer.toByteArray(); + } + + private void writeJsonRpcError(HttpServletResponse resp, String message, JsonNode id) + throws IOException { + String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null"; + String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + -32005 + + ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + resp.setContentType("application/json"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentLength(bytes.length); + resp.getOutputStream().write(bytes); + resp.getOutputStream().flush(); } -} \ No newline at end of file +} diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..41e1bbe43e6 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -375,6 +375,10 @@ node { maxSubTopics = 1000 # Allowed maximum number for blockFilter maxBlockFilterNum = 50000 + # Allowed batch size + maxBatchSize = 1 + # Allowed max response byte size + maxResponseSize = 25 * 1024 * 1024 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, From 02a588f38a5b692a522d9bf51e5067a42060ac85 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 1 Apr 2026 20:52:12 +0800 Subject: [PATCH 02/13] add node.jsonrpc.maxAddressSize and node.jsonrpc.maxRequestTimeout --- .../common/parameter/CommonParameter.java | 6 ++ .../org/tron/core/config/args/NodeConfig.java | 2 + .../JsonRpcResponseTooLargeException.java | 17 +++++ common/src/main/resources/reference.conf | 6 ++ .../java/org/tron/core/config/args/Args.java | 2 + .../filter/BufferedResponseWrapper.java | 22 +++++- .../core/services/jsonrpc/JsonRpcServlet.java | 73 +++++++++++++++---- .../services/jsonrpc/filters/LogFilter.java | 4 + framework/src/main/resources/config.conf | 7 +- 9 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 29f2b909d66..9fcf8debd66 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -465,6 +465,12 @@ public class CommonParameter { @Getter @Setter public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; + @Getter + @Setter + public int jsonRpcMaxRequestTimeout = 30; + @Getter + @Setter + public int jsonRpcMaxAddressSize = 1000; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index c28b37e871c..ab6315c1bbd 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -304,6 +304,8 @@ public void setHttpPBFTPort(int v) { private int maxBlockFilterNum = 50000; private int maxBatchSize = 100; private int maxResponseSize = 25 * 1024 * 1024; + private int maxRequestTimeout = 30; + private int maxAddressSize = 1000; } @Getter diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java new file mode 100644 index 00000000000..65c7bc28cf8 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java @@ -0,0 +1,17 @@ +package org.tron.core.exception.jsonrpc; + +public class JsonRpcResponseTooLargeException extends RuntimeException { + + public JsonRpcResponseTooLargeException() { + super(); + } + + public JsonRpcResponseTooLargeException(String message) { + super(message); + } + + public JsonRpcResponseTooLargeException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index ed5ef5684b3..2764f37e074 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -406,6 +406,12 @@ node { # Maximum response body size in bytes for JSON-RPC (default 25MB) maxResponseSize = 26214400 + + # Maximum request timeout in seconds for JSON-RPC + maxRequestTimeout = 30 + + # Maximum number of addresses in a single JSON-RPC request + maxAddressSize = 1000 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index f13ca7f7303..e1c57dc7a94 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -587,6 +587,8 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); + PARAMETER.jsonRpcMaxRequestTimeout = jsonrpc.getMaxRequestTimeout(); + PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index 2e4d8eb0ef2..fefea565c3c 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -5,22 +5,30 @@ import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; +import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException; /** * Buffers the response body without writing to the underlying response, * so the caller can inspect the size before committing. + * + *

If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw + * {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most + * {@code maxBytes} rather than the full response size. */ public class BufferedResponseWrapper extends HttpServletResponseWrapper { private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final int maxBytes; private final ServletOutputStream outputStream = new ServletOutputStream() { @Override public void write(int b) { + checkLimit(1); buffer.write(b); } @Override public void write(byte[] b, int off, int len) { + checkLimit(len); buffer.write(b, off, len); } @@ -34,8 +42,20 @@ public void setWriteListener(WriteListener writeListener) { } }; - public BufferedResponseWrapper(HttpServletResponse response) { + /** + * @param response the wrapped response + * @param maxBytes max allowed response bytes; {@code 0} means no limit + */ + public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) { super(response); + this.maxBytes = maxBytes; + } + + private void checkLimit(int incoming) { + if (maxBytes > 0 && buffer.size() + incoming > maxBytes) { + throw new JsonRpcResponseTooLargeException( + "Response byte size exceeds the limit of " + maxBytes); + } } @Override diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index fb26c400a24..f7c830cbd45 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; @@ -11,6 +12,12 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -19,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; +import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException; import org.tron.core.services.filter.BufferedResponseWrapper; import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @@ -29,6 +37,21 @@ public class JsonRpcServlet extends RateLimiterServlet { private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ExecutorService RPC_EXECUTOR = Executors.newCachedThreadPool( + new ThreadFactoryBuilder().setNameFormat("jsonrpc-timeout-%d").setDaemon(true).build()); + + enum JsonRpcError { + EXCEED_LIMIT(-32005), + RESPONSE_TOO_LARGE(-32003), + TIMEOUT(-32002); + + final int code; + + JsonRpcError(int code) { + this.code = code; + } + } + private JsonRpcServer rpcServer = null; @Autowired @@ -80,26 +103,50 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // Check batch request array length JsonNode rootNode = MAPPER.readTree(body); if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { - writeJsonRpcError(resp, + writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, "Batch size " + rootNode.size() + " exceeds the limit of " + parameter.getJsonRpcMaxBatchSize(), null); return; } - // Buffer the response to check its size before committing - BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp); - rpcServer.handle(new CachedBodyRequestWrapper(req, body), bufferedResp); + // Buffer the response; limit is enforced eagerly during writes to bound memory usage + int maxResponseSize = parameter.getJsonRpcMaxResponseSize(); + CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body); + BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp, maxResponseSize); + + int timeoutSec = parameter.getJsonRpcMaxRequestTimeout(); + Future future = RPC_EXECUTOR.submit(() -> { + try { + rpcServer.handle(cachedReq, bufferedResp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); - byte[] responseBytes = bufferedResp.toByteArray(); - logger.info("responseBytes: {}", responseBytes.length); - if (responseBytes.length > parameter.getJsonRpcMaxResponseSize()) { + try { + future.get(timeoutSec, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; - writeJsonRpcError(resp, - "Response byte size " + responseBytes.length + " exceeds the limit of " - + parameter.getJsonRpcMaxResponseSize(), idNode); + writeJsonRpcError(resp, JsonRpcError.TIMEOUT, "Request timeout after " + timeoutSec + "s", + idNode); return; + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException + && cause.getCause() instanceof JsonRpcResponseTooLargeException) { + JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; + writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, cause.getCause().getMessage(), + idNode); + return; + } + throw new IOException("RPC execution failed", cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("RPC interrupted", e); } + byte[] responseBytes = bufferedResp.toByteArray(); resp.setContentLength(responseBytes.length); resp.getOutputStream().write(responseBytes); resp.getOutputStream().flush(); @@ -115,10 +162,10 @@ private byte[] readBody(InputStream in) throws IOException { return buffer.toByteArray(); } - private void writeJsonRpcError(HttpServletResponse resp, String message, JsonNode id) - throws IOException { + private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message, + JsonNode id) throws IOException { String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null"; - String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + -32005 + String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + error.code + ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}"; byte[] bytes = body.getBytes(StandardCharsets.UTF_8); resp.setContentType("application/json"); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java index 42bc123d4bc..d2bd58f6c56 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java @@ -50,6 +50,10 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { withContractAddress(addressToByteArray((String) fr.getAddress())); } else if (fr.getAddress() instanceof ArrayList) { + int maxAddressSize = Args.getInstance().getJsonRpcMaxAddressSize(); + if (maxAddressSize > 0 && ((ArrayList) fr.getAddress()).size() > maxAddressSize) { + throw new JsonRpcInvalidParamsException("exceed max addresses: " + maxAddressSize); + } List addr = new ArrayList<>(); int i = 0; for (Object s : (ArrayList) fr.getAddress()) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 41e1bbe43e6..c8b22cfd147 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -369,7 +369,8 @@ node { # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000, # should be > 0, otherwise means no limit. maxBlockRange = 5000 - + # Allowed max address count in filter request + maxAddressSize = 1000 # The maximum number of allowed topics within a topic criteria, default value is 1000, # should be > 0, otherwise means no limit. maxSubTopics = 1000 @@ -378,7 +379,9 @@ node { # Allowed batch size maxBatchSize = 1 # Allowed max response byte size - maxResponseSize = 25 * 1024 * 1024 + maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B + # Allowed max request processing time in seconds + maxRequestTimeout = 30 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, From eff49b91355883fe2ddbc9a4db66f51d8453e024 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 1 Apr 2026 21:03:59 +0800 Subject: [PATCH 03/13] update comment --- framework/src/main/resources/config.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index c8b22cfd147..dcd55698fc4 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -369,7 +369,8 @@ node { # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000, # should be > 0, otherwise means no limit. maxBlockRange = 5000 - # Allowed max address count in filter request + # Allowed max address count in filter request, default value is 1000, + # should be > 0, otherwise means no limit. maxAddressSize = 1000 # The maximum number of allowed topics within a topic criteria, default value is 1000, # should be > 0, otherwise means no limit. From 19217b8605ceb6deddb05e6b310bff7dfe0f429e Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 16 Apr 2026 17:50:56 +0800 Subject: [PATCH 04/13] add jsonRpcMaxBatchSize default as 10 --- .../main/java/org/tron/common/parameter/CommonParameter.java | 2 +- framework/src/main/resources/config.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 9fcf8debd66..c995151248a 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -461,7 +461,7 @@ public class CommonParameter { public int jsonRpcMaxBlockFilterNum = 50000; @Getter @Setter - public int jsonRpcMaxBatchSize = 1; + public int jsonRpcMaxBatchSize = 10; @Getter @Setter public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index dcd55698fc4..a615361d5f8 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -378,7 +378,7 @@ node { # Allowed maximum number for blockFilter maxBlockFilterNum = 50000 # Allowed batch size - maxBatchSize = 1 + maxBatchSize = 10 # Allowed max response byte size maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B # Allowed max request processing time in seconds From 0351c22ccd93e188ee6536941873b9f47cb21f73 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 20 Apr 2026 00:00:18 +0800 Subject: [PATCH 05/13] remove timeout restrict --- .../common/parameter/CommonParameter.java | 3 - .../org/tron/core/config/args/NodeConfig.java | 1 - common/src/main/resources/reference.conf | 3 - .../java/org/tron/core/config/args/Args.java | 1 - .../filter/BufferedResponseWrapper.java | 33 +++++++- .../core/services/jsonrpc/JsonRpcServlet.java | 83 +++++++------------ framework/src/main/resources/config.conf | 2 - 7 files changed, 57 insertions(+), 69 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index c995151248a..db14ba84788 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -467,9 +467,6 @@ public class CommonParameter { public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter - public int jsonRpcMaxRequestTimeout = 30; - @Getter - @Setter public int jsonRpcMaxAddressSize = 1000; @Getter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index ab6315c1bbd..72a4fdb5594 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -304,7 +304,6 @@ public void setHttpPBFTPort(int v) { private int maxBlockFilterNum = 50000; private int maxBatchSize = 100; private int maxResponseSize = 25 * 1024 * 1024; - private int maxRequestTimeout = 30; private int maxAddressSize = 1000; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 2764f37e074..b33e872b68e 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -407,9 +407,6 @@ node { # Maximum response body size in bytes for JSON-RPC (default 25MB) maxResponseSize = 26214400 - # Maximum request timeout in seconds for JSON-RPC - maxRequestTimeout = 30 - # Maximum number of addresses in a single JSON-RPC request maxAddressSize = 1000 } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index e1c57dc7a94..1094b04f2de 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -587,7 +587,6 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize(); PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize(); - PARAMETER.jsonRpcMaxRequestTimeout = jsonrpc.getMaxRequestTimeout(); PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize(); // ---- P2P sub-bean ---- diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index fefea565c3c..b603d27032b 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -1,6 +1,7 @@ package org.tron.core.services.filter; import java.io.ByteArrayOutputStream; +import java.io.IOException; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; @@ -14,11 +15,18 @@ *

If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw * {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most * {@code maxBytes} rather than the full response size. + * + *

Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and + * only forwarded to the real response via {@link #commitToResponse()}, preventing a timed-out + * handler thread from racing with the timeout error writer. */ public class BufferedResponseWrapper extends HttpServletResponseWrapper { + private final HttpServletResponse actual; private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); private final int maxBytes; + private int status = HttpServletResponse.SC_OK; + private String contentType; private final ServletOutputStream outputStream = new ServletOutputStream() { @Override public void write(int b) { @@ -48,6 +56,7 @@ public void setWriteListener(WriteListener writeListener) { */ public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) { super(response); + this.actual = response; this.maxBytes = maxBytes; } @@ -58,6 +67,16 @@ private void checkLimit(int incoming) { } } + @Override + public void setStatus(int sc) { + this.status = sc; + } + + @Override + public void setContentType(String type) { + this.contentType = type; + } + @Override public ServletOutputStream getOutputStream() { return outputStream; @@ -74,8 +93,14 @@ public void setContentLength(int len) { public void setContentLengthLong(long len) { } - public byte[] toByteArray() { - return buffer.toByteArray(); + public void commitToResponse() throws IOException { + if (contentType != null) { + actual.setContentType(contentType); + } + actual.setStatus(status); + byte[] bytes = buffer.toByteArray(); + actual.setContentLength(bytes.length); + actual.getOutputStream().write(bytes); + actual.getOutputStream().flush(); } - -} +} \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index f7c830cbd45..752528ab993 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; @@ -10,14 +10,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -37,13 +30,9 @@ public class JsonRpcServlet extends RateLimiterServlet { private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final ExecutorService RPC_EXECUTOR = Executors.newCachedThreadPool( - new ThreadFactoryBuilder().setNameFormat("jsonrpc-timeout-%d").setDaemon(true).build()); - enum JsonRpcError { EXCEED_LIMIT(-32005), - RESPONSE_TOO_LARGE(-32003), - TIMEOUT(-32002); + RESPONSE_TOO_LARGE(-32003); final int code; @@ -97,10 +86,8 @@ public Integer getJsonRpcCode(int httpStatusCode) { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { CommonParameter parameter = CommonParameter.getInstance(); - // Read request body so we can inspect and replay it - byte[] body = readBody(req.getInputStream()); + byte[] body = readBody(req.getInputStream(), parameter.getJsonRpcMaxResponseSize()); - // Check batch request array length JsonNode rootNode = MAPPER.readTree(body); if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, @@ -109,54 +96,33 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I return; } - // Buffer the response; limit is enforced eagerly during writes to bound memory usage - int maxResponseSize = parameter.getJsonRpcMaxResponseSize(); CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body); - BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(resp, maxResponseSize); - - int timeoutSec = parameter.getJsonRpcMaxRequestTimeout(); - Future future = RPC_EXECUTOR.submit(() -> { - try { - rpcServer.handle(cachedReq, bufferedResp); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper( + resp, parameter.getJsonRpcMaxResponseSize()); try { - future.get(timeoutSec, TimeUnit.SECONDS); - } catch (TimeoutException e) { - future.cancel(true); + rpcServer.handle(cachedReq, bufferedResp); + } catch (JsonRpcResponseTooLargeException e) { JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; - writeJsonRpcError(resp, JsonRpcError.TIMEOUT, "Request timeout after " + timeoutSec + "s", - idNode); + writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, e.getMessage(), idNode); return; - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof RuntimeException - && cause.getCause() instanceof JsonRpcResponseTooLargeException) { - JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; - writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, cause.getCause().getMessage(), - idNode); - return; - } - throw new IOException("RPC execution failed", cause); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("RPC interrupted", e); + } catch (Exception e) { + throw new IOException("RPC execution failed", e); } - byte[] responseBytes = bufferedResp.toByteArray(); - resp.setContentLength(responseBytes.length); - resp.getOutputStream().write(responseBytes); - resp.getOutputStream().flush(); + bufferedResp.commitToResponse(); } - private byte[] readBody(InputStream in) throws IOException { + private byte[] readBody(InputStream in, int maxBytes) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] tmp = new byte[4096]; + int total = 0; int n; while ((n = in.read(tmp)) != -1) { + total += n; + if (maxBytes > 0 && total > maxBytes) { + throw new IOException("Request body exceeds maximum size of " + maxBytes + " bytes"); + } buffer.write(tmp, 0, n); } return buffer.toByteArray(); @@ -164,10 +130,17 @@ private byte[] readBody(InputStream in) throws IOException { private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message, JsonNode id) throws IOException { - String idStr = (id != null && !id.isNull() && !id.isMissingNode()) ? id.toString() : "null"; - String body = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":" + error.code - + ",\"message\":\"" + message + "\"},\"id\":" + idStr + "}"; - byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + ObjectNode root = MAPPER.createObjectNode(); + root.put("jsonrpc", "2.0"); + ObjectNode errNode = root.putObject("error"); + errNode.put("code", error.code); + errNode.put("message", message); + if (id != null && !id.isNull() && !id.isMissingNode()) { + root.set("id", id); + } else { + root.putNull("id"); + } + byte[] bytes = MAPPER.writeValueAsBytes(root); resp.setContentType("application/json"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a615361d5f8..a804ff877ac 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -381,8 +381,6 @@ node { maxBatchSize = 10 # Allowed max response byte size maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B - # Allowed max request processing time in seconds - maxRequestTimeout = 30 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, From acd51cbca3b28cb3ad9b672956a44e5bb97cd501 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 20 Apr 2026 11:19:39 +0800 Subject: [PATCH 06/13] remove input restrict --- .../org/tron/core/services/jsonrpc/JsonRpcServlet.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 752528ab993..c66e3692b0c 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -86,7 +86,7 @@ public Integer getJsonRpcCode(int httpStatusCode) { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { CommonParameter parameter = CommonParameter.getInstance(); - byte[] body = readBody(req.getInputStream(), parameter.getJsonRpcMaxResponseSize()); + byte[] body = readBody(req.getInputStream()); JsonNode rootNode = MAPPER.readTree(body); if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { @@ -113,16 +113,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I bufferedResp.commitToResponse(); } - private byte[] readBody(InputStream in, int maxBytes) throws IOException { + private byte[] readBody(InputStream in) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] tmp = new byte[4096]; - int total = 0; int n; while ((n = in.read(tmp)) != -1) { - total += n; - if (maxBytes > 0 && total > maxBytes) { - throw new IOException("Request body exceeds maximum size of " + maxBytes + " bytes"); - } buffer.write(tmp, 0, n); } return buffer.toByteArray(); From fa87c7efed9c1674d41c9e9d7f8cfaaac69c0fa6 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 20 Apr 2026 11:40:08 +0800 Subject: [PATCH 07/13] optimize ContentType when writeJsonRpcError --- .../java/org/tron/core/services/jsonrpc/JsonRpcServlet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index c66e3692b0c..d239bd708c3 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -136,7 +136,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str root.putNull("id"); } byte[] bytes = MAPPER.writeValueAsBytes(root); - resp.setContentType("application/json"); + resp.setContentType("application/json; charset=utf-8"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); From 01da427d321aa6737d5188ad43829ce1b85878f4 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 23 Apr 2026 17:26:22 +0800 Subject: [PATCH 08/13] add error code -32700 --- .../tron/core/services/jsonrpc/JsonRpcServlet.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index d239bd708c3..a78eec974f1 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -31,6 +31,7 @@ public class JsonRpcServlet extends RateLimiterServlet { private static final ObjectMapper MAPPER = new ObjectMapper(); enum JsonRpcError { + PARSE_ERROR(-32700), EXCEED_LIMIT(-32005), RESPONSE_TOO_LARGE(-32003); @@ -86,9 +87,15 @@ public Integer getJsonRpcCode(int httpStatusCode) { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { CommonParameter parameter = CommonParameter.getInstance(); - byte[] body = readBody(req.getInputStream()); - - JsonNode rootNode = MAPPER.readTree(body); + byte[] body; + JsonNode rootNode; + try { + body = readBody(req.getInputStream()); + rootNode = MAPPER.readTree(body); + } catch (IOException e) { + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse json error", null); + return; + } if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, "Batch size " + rootNode.size() + " exceeds the limit of " From 7b2585d7a32ea510031e781b905a509285de5a5d Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 23 Apr 2026 17:42:55 +0800 Subject: [PATCH 09/13] add getReader for CachedBodyRequestWrapper; set jsonRpcMaxBatchSize default as 100 --- .../org/tron/common/parameter/CommonParameter.java | 2 +- .../core/services/filter/BufferedResponseWrapper.java | 2 +- .../services/filter/CachedBodyRequestWrapper.java | 11 +++++++++++ framework/src/main/resources/config.conf | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index db14ba84788..01ded57ddc9 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -461,7 +461,7 @@ public class CommonParameter { public int jsonRpcMaxBlockFilterNum = 50000; @Getter @Setter - public int jsonRpcMaxBatchSize = 10; + public int jsonRpcMaxBatchSize = 100; @Getter @Setter public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index b603d27032b..9e44da25c0d 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -103,4 +103,4 @@ public void commitToResponse() throws IOException { actual.getOutputStream().write(bytes); actual.getOutputStream().flush(); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java index efee8d9574e..fcda7d34f86 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java @@ -1,6 +1,10 @@ package org.tron.core.services.filter; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; @@ -47,4 +51,11 @@ public void setReadListener(ReadListener readListener) { } }; } + + @Override + public BufferedReader getReader() { + String encoding = getCharacterEncoding(); + Charset charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8; + return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset)); + } } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a804ff877ac..95613b5bf3d 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -378,7 +378,7 @@ node { # Allowed maximum number for blockFilter maxBlockFilterNum = 50000 # Allowed batch size - maxBatchSize = 10 + maxBatchSize = 100 # Allowed max response byte size maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B } From a0f51f8b11227681ab85cc671240b8c5b2e286c2 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 23 Apr 2026 17:57:38 +0800 Subject: [PATCH 10/13] add getWriter() for BufferedResponseWrapper --- .../tron/core/services/filter/BufferedResponseWrapper.java | 6 ++++++ .../java/org/tron/core/services/jsonrpc/JsonRpcServlet.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index 9e44da25c0d..b7a86bafc7b 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -2,6 +2,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintWriter; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; @@ -82,6 +83,11 @@ public ServletOutputStream getOutputStream() { return outputStream; } + @Override + public PrintWriter getWriter() { + return new PrintWriter(outputStream, true); + } + /** * Suppress forwarding Content-Length to the real response; caller sets it after size check. */ diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index a78eec974f1..c345def2d9d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -93,7 +93,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I body = readBody(req.getInputStream()); rootNode = MAPPER.readTree(body); } catch (IOException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse json error", null); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null); return; } if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { From 38edfda4f1f988647b988dc17e12c163c841df97 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 27 Apr 2026 00:07:43 +0800 Subject: [PATCH 11/13] use overflow to replace exception --- .../JsonRpcResponseTooLargeException.java | 17 -- .../filter/BufferedResponseWrapper.java | 69 +++++--- .../core/services/jsonrpc/JsonRpcServlet.java | 12 +- .../filter/BufferedResponseWrapperTest.java | 165 ++++++++++++++++++ 4 files changed, 216 insertions(+), 47 deletions(-) delete mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java create mode 100644 framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java deleted file mode 100644 index 65c7bc28cf8..00000000000 --- a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcResponseTooLargeException.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.tron.core.exception.jsonrpc; - -public class JsonRpcResponseTooLargeException extends RuntimeException { - - public JsonRpcResponseTooLargeException() { - super(); - } - - public JsonRpcResponseTooLargeException(String message) { - super(message); - } - - public JsonRpcResponseTooLargeException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index b7a86bafc7b..01beb05fba2 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -7,15 +7,15 @@ import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; -import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException; +import lombok.Getter; /** * Buffers the response body without writing to the underlying response, - * so the caller can inspect the size before committing. + * so the caller can replay it after the handler returns. * - *

If {@code maxBytes > 0}, writes that would push the buffer past {@code maxBytes} throw - * {@link JsonRpcResponseTooLargeException} immediately, bounding memory usage to at most - * {@code maxBytes} rather than the full response size. + *

If {@code maxBytes > 0} and the response would exceed that limit, the + * {@link #isOverflow()} flag is set instead of throwing. The caller should check this flag after + * the handler returns and write its own error response when true. * *

Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and * only forwarded to the real response via {@link #commitToResponse()}, preventing a timed-out @@ -28,16 +28,31 @@ public class BufferedResponseWrapper extends HttpServletResponseWrapper { private final int maxBytes; private int status = HttpServletResponse.SC_OK; private String contentType; + @Getter + private boolean overflow = false; + private final ServletOutputStream outputStream = new ServletOutputStream() { @Override public void write(int b) { - checkLimit(1); + if (overflow) { + return; + } + if (maxBytes > 0 && buffer.size() >= maxBytes) { + markOverflow(); + return; + } buffer.write(b); } @Override public void write(byte[] b, int off, int len) { - checkLimit(len); + if (overflow) { + return; + } + if (maxBytes > 0 && buffer.size() + len > maxBytes) { + markOverflow(); + return; + } buffer.write(b, off, len); } @@ -61,10 +76,26 @@ public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) { this.maxBytes = maxBytes; } - private void checkLimit(int incoming) { - if (maxBytes > 0 && buffer.size() + incoming > maxBytes) { - throw new JsonRpcResponseTooLargeException( - "Response byte size exceeds the limit of " + maxBytes); + private void markOverflow() { + overflow = true; + buffer.reset(); + } + + /** + * Early-detection path: if the framework reports the full content length before writing any + * bytes, we can flag overflow without buffering anything. + */ + @Override + public void setContentLength(int len) { + if (maxBytes > 0 && len > maxBytes) { + markOverflow(); + } + } + + @Override + public void setContentLengthLong(long len) { + if (maxBytes > 0 && len > maxBytes) { + markOverflow(); } } @@ -88,25 +119,13 @@ public PrintWriter getWriter() { return new PrintWriter(outputStream, true); } - /** - * Suppress forwarding Content-Length to the real response; caller sets it after size check. - */ - @Override - public void setContentLength(int len) { - } - - @Override - public void setContentLengthLong(long len) { - } - public void commitToResponse() throws IOException { if (contentType != null) { actual.setContentType(contentType); } actual.setStatus(status); - byte[] bytes = buffer.toByteArray(); - actual.setContentLength(bytes.length); - actual.getOutputStream().write(bytes); + actual.setContentLength(buffer.size()); + buffer.writeTo(actual.getOutputStream()); actual.getOutputStream().flush(); } } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index c345def2d9d..9bb4901d67c 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; -import org.tron.core.exception.jsonrpc.JsonRpcResponseTooLargeException; import org.tron.core.services.filter.BufferedResponseWrapper; import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @@ -109,14 +108,17 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rpcServer.handle(cachedReq, bufferedResp); - } catch (JsonRpcResponseTooLargeException e) { - JsonNode idNode = (!rootNode.isArray()) ? rootNode.get("id") : null; - writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, e.getMessage(), idNode); - return; } catch (Exception e) { throw new IOException("RPC execution failed", e); } + if (bufferedResp.isOverflow()) { + JsonNode idNode = !rootNode.isArray() ? rootNode.get("id") : null; + writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, + "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes", + idNode); + return; + } bufferedResp.commitToResponse(); } diff --git a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java new file mode 100644 index 00000000000..cf57866e3ab --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java @@ -0,0 +1,165 @@ +package org.tron.core.services.filter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; + +public class BufferedResponseWrapperTest { + + private MockHttpServletResponse mockResp; + + @Before + public void setUp() { + mockResp = new MockHttpServletResponse(); + } + + // --- isOverflow: false cases --- + + @Test + public void noLimit_neverOverflows() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.getOutputStream().write(new byte[1024 * 1024]); + assertFalse(w.isOverflow()); + } + + @Test + public void withinLimit_notOverflow() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10); + w.getOutputStream().write(new byte[10]); + assertFalse(w.isOverflow()); + } + + @Test + public void exactlyAtLimit_notOverflow() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5); + w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5}); + assertFalse(w.isOverflow()); + } + + // --- isOverflow: true via write --- + + @Test + public void oneBytePastLimit_overflow() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5); + w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5, 6}); + assertTrue(w.isOverflow()); + } + + @Test + public void singleByteWrite_triggerOverflow() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 3); + w.getOutputStream().write(1); + w.getOutputStream().write(2); + w.getOutputStream().write(3); + assertFalse(w.isOverflow()); + w.getOutputStream().write(4); + assertTrue(w.isOverflow()); + } + + @Test + public void overflow_bufferIsReleasedOnOverflow() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 4); + w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5}); + assertTrue(w.isOverflow()); + // After overflow, further writes are silently discarded — no exception + w.getOutputStream().write(new byte[100]); + assertTrue(w.isOverflow()); + } + + // --- isOverflow: true via setContentLength --- + + @Test + public void setContentLength_exceedsLimit_overflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setContentLength(101); + assertTrue(w.isOverflow()); + } + + @Test + public void setContentLength_exactlyAtLimit_notOverflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setContentLength(100); + assertFalse(w.isOverflow()); + } + + @Test + public void setContentLengthLong_exceedsLimit_overflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setContentLengthLong(101L); + assertTrue(w.isOverflow()); + } + + @Test + public void setContentLength_noLimit_neverOverflows() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.setContentLength(Integer.MAX_VALUE); + assertFalse(w.isOverflow()); + } + + // --- setContentLength early detection: writes after early overflow are discarded --- + + @Test + public void earlyOverflow_subsequentWritesDiscarded() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10); + w.setContentLength(20); + assertTrue(w.isOverflow()); + w.getOutputStream().write(new byte[5]); + // Nothing committed to actual response + assertFalse(mockResp.isCommitted()); + } + + // --- commitToResponse --- + + @Test + public void commitToResponse_writesBodyAndHeaders() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + w.setStatus(200); + w.setContentType("application/json"); + w.getOutputStream().write(data); + w.commitToResponse(); + + assertEquals(200, mockResp.getStatus()); + assertEquals("application/json", mockResp.getContentType()); + assertArrayEquals(data, mockResp.getContentAsByteArray()); + } + + @Test + public void commitToResponse_setsCorrectContentLength() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + byte[] data = new byte[]{10, 20, 30}; + w.getOutputStream().write(data); + w.commitToResponse(); + + assertEquals(3, mockResp.getContentLength()); + } + + @Test + public void commitToResponse_emptyBuffer_writesZeroBytes() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setStatus(200); + w.commitToResponse(); + + assertEquals(0, mockResp.getContentLength()); + assertEquals(0, mockResp.getContentAsByteArray().length); + } + + // --- header buffering: nothing reaches actual response until commit --- + + @Test + public void statusNotForwardedBeforeCommit() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.setStatus(201); + // MockHttpServletResponse defaults to 200 + assertEquals(200, mockResp.getStatus()); + w.commitToResponse(); + assertEquals(201, mockResp.getStatus()); + } +} From e70ea6b8363c885a69b3a13abb1368fc7d79f1c7 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Mon, 27 Apr 2026 00:21:06 +0800 Subject: [PATCH 12/13] reuse the PrintWriter --- .../tron/core/services/filter/BufferedResponseWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java index 01beb05fba2..46872b15e21 100644 --- a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java +++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java @@ -66,6 +66,8 @@ public void setWriteListener(WriteListener writeListener) { } }; + private final PrintWriter writer = new PrintWriter(outputStream, true); + /** * @param response the wrapped response * @param maxBytes max allowed response bytes; {@code 0} means no limit @@ -116,7 +118,7 @@ public ServletOutputStream getOutputStream() { @Override public PrintWriter getWriter() { - return new PrintWriter(outputStream, true); + return writer; } public void commitToResponse() throws IOException { From bf9a5a13b8e27d9cac5e84317ce415f9ca5696b4 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Tue, 28 Apr 2026 23:33:44 +0800 Subject: [PATCH 13/13] fix default maxResponseSize --- common/src/main/resources/reference.conf | 4 ++-- .../core/services/jsonrpc/JsonRpcServlet.java | 6 +++--- framework/src/main/resources/config.conf | 15 ++++++--------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index b33e872b68e..dc1d4ee91ac 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -404,10 +404,10 @@ node { # Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit maxBatchSize = 100 - # Maximum response body size in bytes for JSON-RPC (default 25MB) + # Maximum response body size in bytes for JSON-RPC (default 25MB), >0 otherwise no limit maxResponseSize = 26214400 - # Maximum number of addresses in a single JSON-RPC request + # Maximum number of addresses in a single JSON-RPC request, >0 otherwise no limit maxAddressSize = 1000 } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 9bb4901d67c..941961cd32b 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -95,10 +95,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null); return; } - if (rootNode.isArray() && rootNode.size() > parameter.getJsonRpcMaxBatchSize()) { + int batchSize = parameter.getJsonRpcMaxBatchSize(); + if (rootNode.isArray() && batchSize > 0 && rootNode.size() > batchSize) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, - "Batch size " + rootNode.size() + " exceeds the limit of " - + parameter.getJsonRpcMaxBatchSize(), null); + "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null); return; } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 95613b5bf3d..1ae2d30c2e5 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -366,20 +366,17 @@ node { # httpPBFTEnable = false # httpPBFTPort = 8565 - # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000, - # should be > 0, otherwise means no limit. + # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, >0 otherwise no limit maxBlockRange = 5000 - # Allowed max address count in filter request, default value is 1000, - # should be > 0, otherwise means no limit. + # Allowed max address count in filter request, default: 1000, >0 otherwise no limit maxAddressSize = 1000 - # The maximum number of allowed topics within a topic criteria, default value is 1000, - # should be > 0, otherwise means no limit. + # The maximum number of allowed topics within a topic criteria, default: 1000, >0 otherwise no limit maxSubTopics = 1000 - # Allowed maximum number for blockFilter + # Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit maxBlockFilterNum = 50000 - # Allowed batch size + # Allowed batch size, default: 100, default: 100, >0 otherwise no limit maxBatchSize = 100 - # Allowed max response byte size + # Allowed max response byte size, default: 26214400, >0 otherwise no limit maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B }