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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 } From c8b53b6c3cd7b5a5b55c2f615be2311c97fa676d Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Tue, 5 May 2026 18:05:38 +0800 Subject: [PATCH 14/31] optimize config file --- .../org/tron/core/config/BeanDefaults.java | 93 ++ .../org/tron/core/config/Configuration.java | 5 +- .../tron/core/config/args/BlockConfig.java | 8 +- .../core/config/args/CommitteeConfig.java | 9 +- .../tron/core/config/args/EventConfig.java | 13 +- .../tron/core/config/args/GenesisConfig.java | 8 +- .../tron/core/config/args/MetricsConfig.java | 11 +- .../org/tron/core/config/args/NodeConfig.java | 15 +- .../core/config/args/RateLimiterConfig.java | 8 +- .../tron/core/config/args/StorageConfig.java | 12 +- .../org/tron/core/config/args/VmConfig.java | 11 +- common/src/main/resources/reference.conf | 835 ------------------ .../tron/core/config/BeanDefaultsTest.java | 216 +++++ framework/src/main/resources/config.conf | 101 ++- .../org/tron/core/config/args/ArgsTest.java | 18 +- 15 files changed, 455 insertions(+), 908 deletions(-) create mode 100644 common/src/main/java/org/tron/core/config/BeanDefaults.java delete mode 100644 common/src/main/resources/reference.conf create mode 100644 common/src/test/java/org/tron/core/config/BeanDefaultsTest.java diff --git a/common/src/main/java/org/tron/core/config/BeanDefaults.java b/common/src/main/java/org/tron/core/config/BeanDefaults.java new file mode 100644 index 00000000000..4c279cee7c2 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/BeanDefaults.java @@ -0,0 +1,93 @@ +package org.tron.core.config; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Generates a Typesafe {@link Config} from a bean instance's current field values. + * + *

Used by each {@code XxxConfig.fromConfig()} to replace the role that + * {@code reference.conf} played: ensures every key ConfigBeanFactory needs is + * present, so a partial user config works without throwing + * {@code ConfigException.Missing}. + * + *

Only public getter+setter pairs (standard JavaBean properties) are included — + * the same set that {@code ConfigBeanFactory.create()} auto-binds. Keys are + * decapitalized exactly as ConfigBeanFactory does: + * {@code Character.toLowerCase(name.charAt(0)) + name.substring(1)}. + * + *

Nested bean fields are recursed into nested HOCON objects. + * {@code List} fields are serialized as HOCON lists (empty by default). + * Fields with no public setter (e.g. {@code @Getter(AccessLevel.NONE)} overrides) + * are automatically skipped — these are handled manually in each + * {@code fromConfig()} via {@code hasPath} guards. + */ +public final class BeanDefaults { + + private BeanDefaults() {} + + /** + * Convert {@code bean}'s public JavaBean properties to a Typesafe Config. + * The resulting Config can be used as a {@code withFallback()} for a user's + * config section to guarantee all keys are present for ConfigBeanFactory. + */ + public static Config toConfig(Object bean) { + return ConfigFactory.parseMap(toMap(bean)); + } + + private static Map toMap(Object bean) { + Map map = new LinkedHashMap<>(); + try { + BeanInfo info = Introspector.getBeanInfo(bean.getClass()); + for (PropertyDescriptor pd : info.getPropertyDescriptors()) { + Method getter = pd.getReadMethod(); + Method setter = pd.getWriteMethod(); + // Skip read-only properties (no setter) — matches ConfigBeanFactory's contract + if (getter == null || setter == null) { + continue; + } + // Use the property name exactly as Introspector produced it. + // ConfigBeanFactory does configProps.get(beanProp.getName()) — the lookup key + // is the property name verbatim, not decapitalized. For ordinary camelCase + // setters (setMaxConnections → "MaxConnections" → decapitalize → "maxConnections") + // Introspector already returns the lowercase form. For setters that start with + // two consecutive uppercase letters (setPBFTEnable → "PBFTEnable") the JavaBean + // spec forbids decapitalization, so pd.getName() == "PBFTEnable" — matching the + // capital-P key that config.conf uses for those fields. + String key = pd.getName(); + Object value = getter.invoke(bean); + map.put(key, toValue(value)); + } + } catch (Exception ignored) { + // Best-effort: any unresolvable field is simply omitted. + // ConfigBeanFactory will throw with a clear path if a required key is missing. + } + return map; + } + + private static Object toValue(Object value) { + if (value == null) { + return ""; + } + if (value instanceof Boolean || value instanceof Number || value instanceof String) { + return value; + } + if (value instanceof List) { + List list = new ArrayList<>(); + for (Object item : (List) value) { + list.add(toValue(item)); + } + return list; + } + // Assume nested bean — recurse so it becomes a nested HOCON object. + return toMap(value); + } +} diff --git a/common/src/main/java/org/tron/core/config/Configuration.java b/common/src/main/java/org/tron/core/config/Configuration.java index 80735290b8c..9870f56a194 100644 --- a/common/src/main/java/org/tron/core/config/Configuration.java +++ b/common/src/main/java/org/tron/core/config/Configuration.java @@ -48,10 +48,11 @@ public static com.typesafe.config.Config getByFileName( private static void resolveConfigFile(String fileName, File confFile) { if (confFile.exists()) { - config = ConfigFactory.parseFile(confFile) - .withFallback(ConfigFactory.defaultReference()); + config = ConfigFactory.parseFile(confFile); } else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName) != null) { + // ConfigFactory.load merges system properties (higher priority than the file), + // which tests rely on to override storage.db.engine via -D flags. config = ConfigFactory.load(fileName); } else { throw new IllegalArgumentException( diff --git a/common/src/main/java/org/tron/core/config/args/BlockConfig.java b/common/src/main/java/org/tron/core/config/args/BlockConfig.java index 4746f390e0c..6ade2b00068 100644 --- a/common/src/main/java/org/tron/core/config/args/BlockConfig.java +++ b/common/src/main/java/org/tron/core/config/args/BlockConfig.java @@ -7,6 +7,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -25,8 +26,6 @@ public class BlockConfig { private long proposalExpireTime = DEFAULT_PROPOSAL_EXPIRE_TIME; private int checkFrozenTime = 1; - // Defaults come from reference.conf (loaded globally via Configuration.java) - /** * Create BlockConfig from the "block" section of the application config. * Also checks that committee.proposalExpireTime is not used (must use block.proposalExpireTime). @@ -38,7 +37,10 @@ public static BlockConfig fromConfig(Config config) { + "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT); } - Config blockSection = config.getConfig("block"); + Config defaults = BeanDefaults.toConfig(new BlockConfig()); + Config blockSection = config.hasPath("block") + ? config.getConfig("block").withFallback(defaults) + : defaults; BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class); blockConfig.postProcess(); return blockConfig; diff --git a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java index 0f94e7a59eb..c2ad56f227c 100644 --- a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -87,8 +88,6 @@ public class CommitteeConfig { // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig - // Defaults come from reference.conf (loaded globally via Configuration.java) - /** * Create CommitteeConfig from the "committee" section of the application config. * @@ -100,8 +99,10 @@ public class CommitteeConfig { private static final String ALLOW_PBFT_KEY = "allowPBFT"; public static CommitteeConfig fromConfig(Config config) { - Config section = config.getConfig("committee"); - + Config defaults = BeanDefaults.toConfig(new CommitteeConfig()); + Config section = config.hasPath("committee") + ? config.getConfig("committee").withFallback(defaults) + : defaults; CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class); // Ensure the manually-named fields get the right values from the original keys cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0; diff --git a/common/src/main/java/org/tron/core/config/args/EventConfig.java b/common/src/main/java/org/tron/core/config/args/EventConfig.java index ac1731de2dc..42c03a04b8f 100644 --- a/common/src/main/java/org/tron/core/config/args/EventConfig.java +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -3,6 +3,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigFactory; +import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -68,8 +69,6 @@ public static class FilterConfig { private List contractTopic = new ArrayList<>(); } - // Defaults come from reference.conf (loaded globally via Configuration.java) - /** * Create EventConfig from the "event.subscribe" section of the application config. * @@ -77,14 +76,20 @@ public static class FilterConfig { * "nativeQueue" but config key is "native". We handle this manually after binding. */ public static EventConfig fromConfig(Config config) { - Config section = config.getConfig("event.subscribe"); + // BeanDefaults covers enable/version/startSyncBlockNum/path/server/dbconfig/ + // contractParse/filter. nativeQueue and topics are excluded (no public setter). + Config defaults = BeanDefaults.toConfig(new EventConfig()); + Config section = config.hasPath("event.subscribe") + ? config.getConfig("event.subscribe") + : ConfigFactory.empty(); // "native" is a Java reserved word, "topics" has optional fields per item — // strip both before binding, read manually String nativeKey = "native"; String topicsKey = "topics"; Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey) - .withoutPath("topicDefaults"); + .withoutPath("topicDefaults") + .withFallback(defaults); EventConfig ec = ConfigBeanFactory.create(bindable, EventConfig.class); // manually bind "native" sub-section diff --git a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java index a17e06d5c0f..74d4338bbb5 100644 --- a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java +++ b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -41,10 +42,11 @@ public static class WitnessConfig { private long voteCount = 0; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - public static GenesisConfig fromConfig(Config config) { - Config section = config.getConfig("genesis.block"); + Config defaults = BeanDefaults.toConfig(new GenesisConfig()); + Config section = config.hasPath("genesis.block") + ? config.getConfig("genesis.block").withFallback(defaults) + : defaults; return ConfigBeanFactory.create(section, GenesisConfig.class); } } diff --git a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java index 5b504acdd1c..98d5c0a36d7 100644 --- a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java +++ b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -35,13 +36,11 @@ public static class InfluxDbConfig { private int metricsReportInterval = 10; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - - /** - * Create MetricsConfig from the "node.metrics" section of the application config. - */ public static MetricsConfig fromConfig(Config config) { - Config section = config.getConfig("node.metrics"); + Config defaults = BeanDefaults.toConfig(new MetricsConfig()); + Config section = config.hasPath("node.metrics") + ? config.getConfig("node.metrics").withFallback(defaults) + : defaults; return ConfigBeanFactory.create(section, MetricsConfig.class); } } 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..4b151d133e5 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 @@ -5,6 +5,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigFactory; +import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -89,8 +90,8 @@ public class NodeConfig { private double activeConnectFactor = 0.1; private double connectFactor = 0.6; // Legacy alias `maxActiveNodesWithSameIp` has no bean field: we only peek at it via - // section.hasPath() below. Keeping it field-less means reference.conf doesn't have to - // ship a default that would otherwise mask the modern `maxConnectionsWithSameIp` key. + // section.hasPath() below. Keeping it field-less means BeanDefaults does not emit a + // default that would mask the modern `maxConnectionsWithSameIp` key. // ---- Sub-beans matching config's dot-notation nested structure ---- private ListenConfig listen = new ListenConfig(); @@ -339,8 +340,6 @@ public static class DnsConfig { private String awsHostZoneId = ""; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - // =========================================================================== // Factory method // =========================================================================== @@ -359,8 +358,10 @@ public static class DnsConfig { * since ConfigBeanFactory expects typed bean lists, not string lists. */ public static NodeConfig fromConfig(Config config) { - Config section = config.getConfig("node"); - + Config defaults = BeanDefaults.toConfig(new NodeConfig()); + Config section = config.hasPath("node") + ? config.getConfig("node").withFallback(defaults) + : defaults; // Auto-bind all fields and sub-beans. ConfigBeanFactory fails fast with a // descriptive path on any `= null` value — external configs that use the // HOCON null keyword should fix their config rather than rely on silent coercion. @@ -386,7 +387,7 @@ public static NodeConfig fromConfig(Config config) { } // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. - // reference.conf does not ship the legacy key, so hasPath here reliably means the user + // BeanDefaults does not emit this legacy key, so hasPath here reliably means the user // set it in their config. When present, it overrides the modern key. if (section.hasPath("fullNodeAllowShieldedTransaction")) { nc.allowShieldedTransactionApi = section.getBoolean("fullNodeAllowShieldedTransaction"); diff --git a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java index eed5ef1898b..8fda4fba49c 100644 --- a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -66,10 +67,11 @@ public static class RpcRateLimitItem { private String paramString = ""; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - public static RateLimiterConfig fromConfig(Config config) { - Config section = config.getConfig("rate.limiter"); + Config defaults = BeanDefaults.toConfig(new RateLimiterConfig()); + Config section = config.hasPath("rate.limiter") + ? config.getConfig("rate.limiter").withFallback(defaults) + : defaults; return ConfigBeanFactory.create(section, RateLimiterConfig.class); } } diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 2517f4d10d7..ad8a41d35d2 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import com.typesafe.config.ConfigObject; import java.util.ArrayList; import java.util.List; @@ -205,11 +206,14 @@ public static class PropertyConfig { private int maxOpenFiles = 100; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - public static StorageConfig fromConfig(Config config) { - Config section = config.getConfig("storage"); - + Config defaults = BeanDefaults.toConfig(new StorageConfig()); + // User's storage section takes priority; defaults fill in any omitted scalar keys. + // readDbOption() uses hasPath() on the merged section, so user-set optional keys + // (default, defaultM, defaultL) are still detected correctly. + Config section = config.hasPath("storage") + ? config.getConfig("storage").withFallback(defaults) + : defaults; StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); sc.rawStorageConfig = section; diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index d583cf4c601..a4b219f8726 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -28,13 +29,11 @@ public class VmConfig { private boolean saveFeaturedInternalTx = false; private boolean saveCancelAllUnfreezeV2Details = false; - /** - * Create VmConfig from the "vm" section of the application config. - * Defaults come from reference.conf (loaded globally via Configuration.java), - * so no per-bean DEFAULTS needed. - */ public static VmConfig fromConfig(Config config) { - Config vmSection = config.getConfig("vm"); + Config defaults = BeanDefaults.toConfig(new VmConfig()); + Config vmSection = config.hasPath("vm") + ? config.getConfig("vm").withFallback(defaults) + : defaults; VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); vmConfig.postProcess(); return vmConfig; diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf deleted file mode 100644 index 11970a0a673..00000000000 --- a/common/src/main/resources/reference.conf +++ /dev/null @@ -1,835 +0,0 @@ -# ============================================================================= -# reference.conf — Full default configuration for java-tron -# ============================================================================= -# -# This file defines the default value for every configuration parameter. -# It is packaged inside the jar and loaded automatically via Typesafe Config's -# standard mechanism: ConfigFactory.defaultReference(). -# -# Loading priority (highest wins): -# 1. User's external config file (e.g. config.conf passed via -c flag) -# 2. This file (reference.conf, bundled in jar) -# -# When a user's config.conf omits a parameter, the value from this file is -# used as the fallback. This ensures the node always has a complete and valid -# configuration, even if the user only overrides a few parameters. -# -# Maintenance rules: -# - Every parameter that the code reads must have an entry here -# - Values must match the bean field initializers in the corresponding -# XxxConfig.java classes (VmConfig, NodeConfig, CommitteeConfig, etc.) -# - Keep the section order and key order identical to config.conf for -# easy side-by-side comparison -# - When adding a new parameter: add it here AND in the bean class -# -# Key naming rules (required for ConfigBeanFactory auto-binding): -# - Use standard camelCase: maxConnections, syncFetchBatchNum, etc. -# -# Keys that cannot auto-bind (handled manually in bean fromConfig): -# -# 1. committee.pBFTExpireNum — lowercase "p" then uppercase "BFT": -# setPBFTExpireNum -> property "PBFTExpireNum" (capital P), -# mismatches config key "pBFTExpireNum" (lowercase p). -# -# 2. node.isOpenFullTcpDisconnect — boolean "is" prefix: -# getter isOpenFullTcpDisconnect() -> property "openFullTcpDisconnect", -# mismatches config key "isOpenFullTcpDisconnect". -# -# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — PascalCase keys: -# setBlockTime -> property "blockTime", mismatches "BlockTime". -# -# ============================================================================= - -net { - # type is deprecated and has no effect. - # type = mainnet -} - -storage { - # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) - db.engine = "LEVELDB" - db.sync = false - db.directory = "database" - - # Index directory (legacy, not consumed by any runtime code, kept for CLI/test compatibility) - index.directory = "index" - index.switch = "on" - - # Whether to write transaction result in transactionRetStore - transHistory.switch = "on" - - # Per-database LevelDB option overrides. Default: empty (all databases use global defaults). - # setting can improve leveldb performance .... start, deprecated for arm - # node: if this will increase process fds, you may check your ulimit if 'too many open files' error occurs - # see https://github.com/tronprotocol/tips/blob/master/tip-343.md for detail - # if you find block sync has lower performance, you can try this settings - # default = { - # maxOpenFiles = 100 - # } - # defaultM = { - # maxOpenFiles = 500 - # } - # defaultL = { - # maxOpenFiles = 1000 - # } - # setting can improve leveldb performance .... end, deprecated for arm - - # Example per-database overrides: - # { - # name = "account", - # path = "storage_directory_test", - # createIfMissing = true, - # paranoidChecks = true, - # verifyChecksums = true, - # compressionType = 1, // compressed with snappy - # blockSize = 4096, // 4 KB = 4 * 1024 B - # writeBufferSize = 10485760, // 10 MB = 10 * 1024 * 1024 B - # cacheSize = 10485760, // 10 MB = 10 * 1024 * 1024 B - # maxOpenFiles = 100 - # } - properties = [] - - needToUpdateAsset = true - - # RocksDB settings (only used when db.engine = "ROCKSDB") - # Strongly recommend NOT modifying unless you know every item's meaning clearly. - dbSettings = { - levelNumber = 7 - compactThreads = 0 // 0 = auto: max(availableProcessors, 1) - blocksize = 16 // n * KB - maxBytesForLevelBase = 256 // n * MB - maxBytesForLevelMultiplier = 10 - level0FileNumCompactionTrigger = 2 - targetFileSizeBase = 64 // n * MB - targetFileSizeMultiplier = 1 - maxOpenFiles = 5000 - } - - balance.history.lookup = false - - # Checkpoint version for snapshot mechanism. Version 2 enables V2 snapshot. - checkpoint.version = 1 - checkpoint.sync = true - - # Estimated number of block transactions (default 1000, min 100, max 10000). - # Total cached transactions = 65536 * txCache.estimatedTransactions - txCache.estimatedTransactions = 1000 - # If true, transaction cache initialization will be faster. - txCache.initOptimization = false - - # Number of blocks flushed to db in each batch during node syncing. - snapshot.maxFlushCount = 1 - - # Database backup settings (RocksDB only) - backup = { - enable = false - propPath = "prop.properties" - bak1path = "bak1/database/" - bak2path = "bak2/database/" - frequency = 10000 - } - - # Data root setting, for check data, currently only reward-vi is used. - # merkleRoot = { - # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net - # } -} - -node.discovery = { - enable = false - persist = false - external.ip = "" -} - -# Custom stop condition -# node.shutdown = { -# BlockTime = "54 59 08 * * ?" # if block header time in persistent db matched -# BlockHeight = 33350800 # if block header height in persistent db matched -# BlockCount = 12 # block sync count after node start -# } - -node.backup { - port = 10001 - priority = 0 - keepAliveInterval = 3000 - members = [ - # "ip", - # "ip" - ] -} - -# Algorithm for generating public key from private key. Do not modify to avoid forks. -crypto { - engine = "eckey" -} - -# Energy limit block number (config key has typo "enery" preserved for backward compatibility) -enery.limit.block.num = 4727890 - -# Actuator whitelist — empty means all actuators allowed -actuator { - whitelist = [] -} - -node.metrics = { - prometheus { - enable = false - port = 9527 - } - - storageEnable = false - - influxdb { - ip = "" - port = 8086 - database = "metrics" - metricsReportInterval = 10 - } -} - -node { - # Trust node for solidity node (example: "127.0.0.1:50051"). - # Empty string here = "not configured"; Args.java bridge converts "" → null so the - # runtime behavior matches develop (trustNodeAddr is null unless user sets the key). - trustNode = "" - - # Expose extension api to public or not - walletExtensionApi = false - - listen.port = 18888 - connection.timeout = 2 - fetchBlock.timeout = 500 - - # Number of blocks to fetch in one batch during sync. Range: [100, 2000]. - syncFetchBatchNum = 2000 - - # Number of validate sign threads, default availableProcessors - # Number of validate sign threads, 0 = auto (availableProcessors) - validateSignThreadNum = 0 - - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - maxConnectionsWithSameIp = 2 - maxHttpConnectNumber = 50 - minParticipationRate = 0 - - # Whether to enable shielded transaction API - allowShieldedTransactionApi = true - - # Whether to print config log at startup - openPrintLog = true - - # If true, SR packs transactions into a block in descending order of fee; - # otherwise, packs by receive timestamp. - openTransactionSort = false - - # Threshold for broadcast transactions received from each peer per second, - # transactions exceeding this are discarded - maxTps = 1000 - - isOpenFullTcpDisconnect = false - inactiveThreshold = 600 // seconds - tcpNettyWorkThreadNum = 0 - udpNettyWorkThreadNum = 1 - maxFastForwardNum = 4 - activeConnectFactor = 0.1 - connectFactor = 0.6 - # Legacy alias `maxActiveNodesWithSameIp` is still accepted from user config - # (see NodeConfig alias-fallback) but is intentionally NOT defaulted here — - # shipping it in reference.conf would always mask the modern `maxConnectionsWithSameIp`. - channel.read.timeout = 0 - metricsEnable = false - - p2p { - version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 - } - - active = [ - # Active establish connection in any case - # "ip:port", - # "ip:port" - ] - - passive = [ - # Passive accept connection in any case - # "ip:port", - # "ip:port" - ] - - fastForward = [ - "100.27.171.62:18888", - "15.188.6.125:18888" - ] - - http { - fullNodeEnable = true - fullNodePort = 8090 - solidityEnable = true - solidityPort = 8091 - PBFTEnable = true - PBFTPort = 8092 - } - - rpc { - enable = true - port = 50051 - solidityEnable = true - solidityPort = 50061 - PBFTEnable = true - PBFTPort = 50071 - - # Number of gRPC threads, 0 = auto (availableProcessors / 2) - thread = 0 - - # Maximum concurrent calls per incoming connection - # No limit on concurrent calls per connection - maxConcurrentCallsPerConnection = 2147483647 - - # HTTP/2 flow control window (bytes), default 1MB - flowControlWindow = 1048576 - - # Connection idle timeout (ms). No limit by default. - maxConnectionIdleInMillis = 9223372036854775807 - - # Connection max age (ms). No limit by default. - maxConnectionAgeInMillis = 9223372036854775807 - - # Maximum message size (bytes), default 4MB - maxMessageSize = 4194304 - - # Maximum header list size (bytes), default 8192 - maxHeaderListSize = 8192 - - # RST_STREAM frames allowed per connection per period, 0 = no limit - maxRstStream = 0 - - # Seconds per period for gRPC RST_STREAM limit - secondsPerWindow = 0 - - # Minimum effective connections required to broadcast transactions - minEffectiveConnection = 1 - - # Reflection service switch for grpcurl tool - reflectionService = false - trxCacheEnable = false - } - - # Number of solidity threads in FullNode. - # Increase if solidity rpc/http interface timeouts occur. - # Default: number of cpu cores. - # Number of solidity threads, 0 = auto (availableProcessors) - solidity.threads = 0 - - # Maximum percentage of producing block interval (provides time for broadcast etc.) - blockProducedTimeOut = 50 - - # Maximum transactions from network layer per second - netMaxTrxPerSecond = 700 - - # Whether to enable node detection function - nodeDetectEnable = false - - # Use IPv6 address for node discovery and TCP connection - enableIpv6 = false - - # If node's highest block is below all peers, try to acquire new connection - effectiveCheckEnable = false - - # Dynamic loading configuration function - dynamicConfig = { - enable = false - checkInterval = 600 - } - - # Block solidification check - unsolidifiedBlockCheck = false - maxUnsolidifiedBlocks = 54 - blockCacheTimeout = 60 - - # TCP and transaction limits - receiveTcpMinDataLength = 2048 - maxTransactionPendingSize = 2000 - pendingTransactionTimeout = 60000 - - # Consensus agreement - agreeNodeCount = 0 - - # Shielded transaction (ZK) - zenTokenId = "000000" - shieldedTransInPendingMaxCounts = 10 - - # Contract proto validation thread pool (0 = auto: availableProcessors) - validContractProto.threads = 0 - - dns { - treeUrls = [ - # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", - ] - publish = false - dnsDomain = "" - dnsPrivate = "" - knownUrls = [] - staticNodes = [] - maxMergeSize = 0 - changeThreshold = 0.0 - serverType = "" - accessKeyId = "" - accessKeySecret = "" - aliyunDnsEndpoint = "" - awsRegion = "" - awsHostZoneId = "" - } - - # Open history query APIs on lite FullNode (may return null for some queries) - openHistoryQueryWhenLiteFN = false - - jsonrpc { - httpFullNodeEnable = false - httpFullNodePort = 8545 - httpSolidityEnable = false - httpSolidityPort = 8555 - httpPBFTEnable = false - httpPBFTPort = 8565 - - # Maximum blocks range for eth_getLogs, >0 otherwise no limit - maxBlockRange = 5000 - - # Maximum topics within a topic criteria, >0 otherwise no limit - maxSubTopics = 1000 - - # Maximum number for blockFilter - maxBlockFilterNum = 50000 - } - - # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. - disabledApi = [ - # "getaccount", - # "getnowblock2" - ] -} - -## Rate limiter config -rate.limiter = { - # Strategies: GlobalPreemptibleAdapter, QpsRateLimiterAdapter, IPQPSRateLimiterAdapter - # Default: QpsRateLimiterAdapter with qps=1000 - - http = [ - # { - # component = "GetNowBlockServlet", - # strategy = "GlobalPreemptibleAdapter", - # paramString = "permit=1" - # }, - # { - # component = "GetAccountServlet", - # strategy = "IPQPSRateLimiterAdapter", - # paramString = "qps=1" - # }, - # { - # component = "ListWitnessesServlet", - # strategy = "QpsRateLimiterAdapter", - # paramString = "qps=1" - # } - ] - - rpc = [ - # { - # component = "protocol.Wallet/GetBlockByLatestNum2", - # strategy = "GlobalPreemptibleAdapter", - # paramString = "permit=1" - # }, - # { - # component = "protocol.Wallet/GetAccount", - # strategy = "IPQPSRateLimiterAdapter", - # paramString = "qps=1" - # }, - # { - # component = "protocol.Wallet/ListWitnesses", - # strategy = "QpsRateLimiterAdapter", - # paramString = "qps=1" - # } - ] - - p2p = { - syncBlockChain = 3.0 - fetchInvData = 3.0 - disconnect = 1.0 - } - - global.qps = 50000 - global.ip.qps = 10000 - global.api.qps = 1000 -} - -seed.node = { - ip.list = [ - "3.225.171.164:18888", - "52.8.46.215:18888", - "3.79.71.167:18888", - "108.128.110.16:18888", - "18.133.82.227:18888", - "35.180.81.133:18888", - "13.210.151.5:18888", - "18.231.27.82:18888", - "3.12.212.122:18888", - "52.24.128.7:18888", - "15.207.144.3:18888", - "3.39.38.55:18888", - "54.151.226.240:18888", - "35.174.93.198:18888", - "18.210.241.149:18888", - "54.177.115.127:18888", - "54.254.131.82:18888", - "18.167.171.167:18888", - "54.167.11.177:18888", - "35.74.7.196:18888", - "52.196.244.176:18888", - "54.248.129.19:18888", - "43.198.142.160:18888", - "3.0.214.7:18888", - "54.153.59.116:18888", - "54.153.94.160:18888", - "54.82.161.39:18888", - "54.179.207.68:18888", - "18.142.82.44:18888", - "18.163.230.203:18888", - # "[2a05:d014:1f2f:2600:1b15:921:d60b:4c60]:18888", // use this if support ipv6 - # "[2600:1f18:7260:f400:8947:ebf3:78a0:282b]:18888", // use this if support ipv6 - ] -} - -genesis.block = { - assets = [ - { - accountName = "Zion" - accountType = "AssetIssue" - address = "TLLM21wteSPs4hKjbxgmH1L6poyMjeTbHm" - balance = "99000000000000000" - }, - { - accountName = "Sun" - accountType = "AssetIssue" - address = "TXmVpin5vq5gdZsciyyjdZgKRUju4st1wM" - balance = "0" - }, - { - accountName = "Blackhole" - accountType = "AssetIssue" - address = "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" - balance = "-9223372036854775808" - } - ] - - witnesses = [ - { - address: THKJYuUmMKKARNf7s2VT51g5uPY6KEqnat, - url = "http://GR1.com", - voteCount = 100000026 - }, - { - address: TVDmPWGYxgi5DNeW8hXrzrhY8Y6zgxPNg4, - url = "http://GR2.com", - voteCount = 100000025 - }, - { - address: TWKZN1JJPFydd5rMgMCV5aZTSiwmoksSZv, - url = "http://GR3.com", - voteCount = 100000024 - }, - { - address: TDarXEG2rAD57oa7JTK785Yb2Et32UzY32, - url = "http://GR4.com", - voteCount = 100000023 - }, - { - address: TAmFfS4Tmm8yKeoqZN8x51ASwdQBdnVizt, - url = "http://GR5.com", - voteCount = 100000022 - }, - { - address: TK6V5Pw2UWQWpySnZyCDZaAvu1y48oRgXN, - url = "http://GR6.com", - voteCount = 100000021 - }, - { - address: TGqFJPFiEqdZx52ZR4QcKHz4Zr3QXA24VL, - url = "http://GR7.com", - voteCount = 100000020 - }, - { - address: TC1ZCj9Ne3j5v3TLx5ZCDLD55MU9g3XqQW, - url = "http://GR8.com", - voteCount = 100000019 - }, - { - address: TWm3id3mrQ42guf7c4oVpYExyTYnEGy3JL, - url = "http://GR9.com", - voteCount = 100000018 - }, - { - address: TCvwc3FV3ssq2rD82rMmjhT4PVXYTsFcKV, - url = "http://GR10.com", - voteCount = 100000017 - }, - { - address: TFuC2Qge4GxA2U9abKxk1pw3YZvGM5XRir, - url = "http://GR11.com", - voteCount = 100000016 - }, - { - address: TNGoca1VHC6Y5Jd2B1VFpFEhizVk92Rz85, - url = "http://GR12.com", - voteCount = 100000015 - }, - { - address: TLCjmH6SqGK8twZ9XrBDWpBbfyvEXihhNS, - url = "http://GR13.com", - voteCount = 100000014 - }, - { - address: TEEzguTtCihbRPfjf1CvW8Euxz1kKuvtR9, - url = "http://GR14.com", - voteCount = 100000013 - }, - { - address: TZHvwiw9cehbMxrtTbmAexm9oPo4eFFvLS, - url = "http://GR15.com", - voteCount = 100000012 - }, - { - address: TGK6iAKgBmHeQyp5hn3imB71EDnFPkXiPR, - url = "http://GR16.com", - voteCount = 100000011 - }, - { - address: TLaqfGrxZ3dykAFps7M2B4gETTX1yixPgN, - url = "http://GR17.com", - voteCount = 100000010 - }, - { - address: TX3ZceVew6yLC5hWTXnjrUFtiFfUDGKGty, - url = "http://GR18.com", - voteCount = 100000009 - }, - { - address: TYednHaV9zXpnPchSywVpnseQxY9Pxw4do, - url = "http://GR19.com", - voteCount = 100000008 - }, - { - address: TCf5cqLffPccEY7hcsabiFnMfdipfyryvr, - url = "http://GR20.com", - voteCount = 100000007 - }, - { - address: TAa14iLEKPAetX49mzaxZmH6saRxcX7dT5, - url = "http://GR21.com", - voteCount = 100000006 - }, - { - address: TBYsHxDmFaRmfCF3jZNmgeJE8sDnTNKHbz, - url = "http://GR22.com", - voteCount = 100000005 - }, - { - address: TEVAq8dmSQyTYK7uP1ZnZpa6MBVR83GsV6, - url = "http://GR23.com", - voteCount = 100000004 - }, - { - address: TRKJzrZxN34YyB8aBqqPDt7g4fv6sieemz, - url = "http://GR24.com", - voteCount = 100000003 - }, - { - address: TRMP6SKeFUt5NtMLzJv8kdpYuHRnEGjGfe, - url = "http://GR25.com", - voteCount = 100000002 - }, - { - address: TDbNE1VajxjpgM5p7FyGNDASt3UVoFbiD3, - url = "http://GR26.com", - voteCount = 100000001 - }, - { - address: TLTDZBcPoJ8tZ6TTEeEqEvwYFk2wgotSfD, - url = "http://GR27.com", - voteCount = 100000000 - } - ] - - timestamp = "0" - - parentHash = "0xe58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f" -} - -# Optional. Used when the witness account has set witnessPermission. -# localWitnessAccountAddress = - -localwitness = [ -] - -# localwitnesskeystore = [ -# "localwitnesskeystore.json" -# ] - -block = { - needSyncCheck = false - maintenanceTimeInterval = 21600000 // 6 hours (ms) - proposalExpireTime = 259200000 // 3 days (ms), controlled by committee proposal - checkFrozenTime = 1 // maintenance periods to check frozen balance (test only) -} - -# Transaction reference block: "solid" or "head". Default "solid". "head" may cause TaPos error. -trx.reference.block = "solid" - -# Transaction expiration time in milliseconds. -trx.expiration.timeInMilliseconds = 60000 - -vm = { - supportConstant = false - maxEnergyLimitForConstant = 100000000 - minTimeRatio = 0.0 - maxTimeRatio = 5.0 - saveInternalTx = false - lruCacheSize = 500 - vmTrace = false - - # Whether to store featured internal transactions (freeze, vote, etc.) - saveFeaturedInternalTx = false - - # Whether to store details of CANCELALLUNFREEZEV2 opcode internal transactions - saveCancelAllUnfreezeV2Details = false - - # Max execution time (ms) for re-executed transactions during packaging - longRunningTime = 10 - - # Whether to support estimate energy API - estimateEnergy = false - - # Max retry time for executing transaction in estimating energy - estimateEnergyMaxRetry = 3 -} - -# Governance proposal toggle parameters. All default to 0 (disabled). -# Controlled by on-chain committee proposals, not manual configuration. -# Setting them in config is only for private chain testing. -committee = { - allowCreationOfContracts = 0 - allowMultiSign = 0 - allowAdaptiveEnergy = 0 - allowDelegateResource = 0 - allowSameTokenName = 0 - allowTvmTransferTrc10 = 0 - allowTvmConstantinople = 0 - allowTvmSolidity059 = 0 - forbidTransferToContract = 0 - allowShieldedTRC20Transaction = 0 - allowTvmIstanbul = 0 - allowMarketTransaction = 0 - allowProtoFilterNum = 0 - allowAccountStateRoot = 0 - changedDelegation = 0 - allowPBFT = 0 - pBFTExpireNum = 20 - allowTransactionFeePool = 0 - allowBlackHoleOptimization = 0 - allowNewResourceModel = 0 - allowReceiptsMerkleRoot = 0 - allowTvmFreeze = 0 - allowTvmVote = 0 - unfreezeDelayDays = 0 - allowTvmLondon = 0 - allowTvmCompatibleEvm = 0 - allowHigherLimitForMaxCpuTimeOfOneTx = 0 - allowNewRewardAlgorithm = 0 - allowOptimizedReturnValueOfChainId = 0 - allowTvmShangHai = 0 - allowOldRewardOpt = 0 - allowEnergyAdjustment = 0 - allowStrictMath = 0 - consensusLogicOptimization = 0 - allowTvmCancun = 0 - allowTvmBlob = 0 - allowTvmOsaka = 0 - allowAccountAssetOptimization = 0 - allowAssetOptimization = 0 - allowNewReward = 0 - memoFee = 0 - allowDelegateOptimization = 0 - allowDynamicEnergy = 0 - dynamicEnergyThreshold = 0 - dynamicEnergyIncreaseFactor = 0 - dynamicEnergyMaxFactor = 0 -} - -event.subscribe = { - enable = false - - native = { - useNativeQueue = true - bindport = 5555 - sendqueuelength = 1000 - } - - version = 0 - startSyncBlockNum = 0 - path = "" - server = "" - dbconfig = "" - contractParse = true - - topics = [ - { - triggerName = "block" - enable = false - topic = "block" - solidified = false - }, - { - triggerName = "transaction" - enable = false - topic = "transaction" - solidified = false - ethCompatible = false - }, - { - triggerName = "contractevent" - enable = false - topic = "contractevent" - }, - { - triggerName = "contractlog" - enable = false - topic = "contractlog" - redundancy = false - }, - { - triggerName = "solidity" - enable = true - topic = "solidity" - }, - { - triggerName = "solidityevent" - enable = false - topic = "solidityevent" - }, - { - triggerName = "soliditylog" - enable = false - topic = "soliditylog" - redundancy = false - } - ] - - filter = { - fromblock = "" - toblock = "" - contractAddress = [ - "" - ] - contractTopic = [ - "" - ] - } -} diff --git a/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java new file mode 100644 index 00000000000..c4170d580c1 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java @@ -0,0 +1,216 @@ +package org.tron.core.config; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; +import org.junit.Assert; +import org.junit.Test; +import org.tron.core.config.args.CommitteeConfig; +import org.tron.core.config.args.MetricsConfig; +import org.tron.core.config.args.NodeConfig; +import org.tron.core.config.args.RateLimiterConfig; +import org.tron.core.config.args.StorageConfig; +import org.tron.core.config.args.VmConfig; + +/** + * Verifies that BeanDefaults.toConfig() produces a Config that: + * 1. Contains the correct default values from Java field initializers. + * 2. Satisfies ConfigBeanFactory.create() without ConfigException.Missing. + * 3. Is properly overridden when a user value is supplied via withFallback(). + */ +public class BeanDefaultsTest { + + // ── VmConfig ───────────────────────────────────────────────────────────── + + @Test + public void vmConfig_defaultValues() { + Config cfg = BeanDefaults.toConfig(new VmConfig()); + + Assert.assertFalse(cfg.getBoolean("supportConstant")); + Assert.assertEquals(100_000_000L, cfg.getLong("maxEnergyLimitForConstant")); + Assert.assertEquals(500, cfg.getInt("lruCacheSize")); + Assert.assertEquals(0.0, cfg.getDouble("minTimeRatio"), 0.0); + Assert.assertEquals(5.0, cfg.getDouble("maxTimeRatio"), 0.0); + Assert.assertEquals(10, cfg.getInt("longRunningTime")); + Assert.assertFalse(cfg.getBoolean("estimateEnergy")); + Assert.assertEquals(3, cfg.getInt("estimateEnergyMaxRetry")); + Assert.assertFalse(cfg.getBoolean("vmTrace")); + Assert.assertFalse(cfg.getBoolean("saveInternalTx")); + Assert.assertFalse(cfg.getBoolean("saveFeaturedInternalTx")); + Assert.assertFalse(cfg.getBoolean("saveCancelAllUnfreezeV2Details")); + } + + @Test + public void vmConfig_roundTrip_withConfigBeanFactory() { + Config defaults = BeanDefaults.toConfig(new VmConfig()); + // ConfigBeanFactory must not throw ConfigException.Missing + VmConfig vm = ConfigBeanFactory.create(defaults, VmConfig.class); + Assert.assertFalse(vm.isSupportConstant()); + Assert.assertEquals(500, vm.getLruCacheSize()); + } + + @Test + public void vmConfig_userValueOverridesDefault() { + Config user = ConfigFactory.parseString("lruCacheSize = 999"); + Config merged = user.withFallback(BeanDefaults.toConfig(new VmConfig())); + VmConfig vm = ConfigBeanFactory.create(merged, VmConfig.class); + Assert.assertEquals(999, vm.getLruCacheSize()); + // other fields keep defaults + Assert.assertEquals(10, vm.getLongRunningTime()); + } + + // ── NodeConfig nested bean ──────────────────────────────────────────────── + + @Test + public void nodeConfig_defaultScalars() { + Config cfg = BeanDefaults.toConfig(new NodeConfig()); + + Assert.assertEquals(30, cfg.getInt("maxConnections")); + Assert.assertEquals(8, cfg.getInt("minConnections")); + Assert.assertEquals(1000, cfg.getInt("maxTps")); + Assert.assertTrue(cfg.getBoolean("openPrintLog")); + Assert.assertFalse(cfg.getBoolean("walletExtensionApi")); + } + + @Test + public void nodeConfig_nestedBeans_present() { + Config cfg = BeanDefaults.toConfig(new NodeConfig()); + + // listen.port should exist as a nested object + Assert.assertTrue(cfg.hasPath("listen")); + Assert.assertEquals(18888, cfg.getInt("listen.port")); + + // discovery.enable + Assert.assertTrue(cfg.hasPath("discovery")); + Assert.assertFalse(cfg.getBoolean("discovery.enable")); + + // http.fullNodeEnable + Assert.assertTrue(cfg.hasPath("http")); + Assert.assertTrue(cfg.getBoolean("http.fullNodeEnable")); + Assert.assertEquals(8090, cfg.getInt("http.fullNodePort")); + + // rpc.enable + Assert.assertTrue(cfg.hasPath("rpc")); + Assert.assertTrue(cfg.getBoolean("rpc.enable")); + Assert.assertEquals(50051, cfg.getInt("rpc.port")); + } + + @Test + public void nodeConfig_listFields_empty() { + Config cfg = BeanDefaults.toConfig(new NodeConfig()); + Assert.assertTrue(cfg.getList("active").isEmpty()); + Assert.assertTrue(cfg.getList("passive").isEmpty()); + Assert.assertTrue(cfg.getList("fastForward").isEmpty()); + Assert.assertTrue(cfg.getList("disabledApi").isEmpty()); + } + + @Test + public void nodeConfig_pBFTFields_usePropertyNameAsIs() { + Config cfg = BeanDefaults.toConfig(new NodeConfig()); + // setPBFTEnable → Introspector property name "PBFTEnable" (capital P, two consecutive + // uppercase letters → JavaBean spec forbids decapitalization). + // ConfigBeanFactory looks up configProps.get("PBFTEnable"), so the map key must match. + Assert.assertTrue(cfg.hasPath("http.PBFTEnable")); + Assert.assertTrue(cfg.getBoolean("http.PBFTEnable")); + Assert.assertEquals(8092, cfg.getInt("http.PBFTPort")); + + Assert.assertTrue(cfg.hasPath("rpc.PBFTEnable")); + Assert.assertEquals(50071, cfg.getInt("rpc.PBFTPort")); + } + + @Test + public void nodeConfig_roundTrip_withConfigBeanFactory() { + Config defaults = BeanDefaults.toConfig(new NodeConfig()); + // Must not throw — all keys present + NodeConfig nc = ConfigBeanFactory.create(defaults, NodeConfig.class); + Assert.assertEquals(30, nc.getMaxConnections()); + Assert.assertEquals(18888, nc.getListenPort()); + Assert.assertTrue(nc.getRpc().isEnable()); + } + + // ── StorageConfig nested bean ───────────────────────────────────────────── + + @Test + public void storageConfig_defaultValues() { + Config cfg = BeanDefaults.toConfig(new StorageConfig()); + + Assert.assertEquals("LEVELDB", cfg.getString("db.engine")); + Assert.assertFalse(cfg.getBoolean("db.sync")); + Assert.assertEquals("database", cfg.getString("db.directory")); + Assert.assertEquals(7, cfg.getInt("dbSettings.levelNumber")); + Assert.assertEquals(1, cfg.getInt("checkpoint.version")); + Assert.assertTrue(cfg.getBoolean("checkpoint.sync")); + Assert.assertEquals(1, cfg.getInt("snapshot.maxFlushCount")); + Assert.assertTrue(cfg.getList("properties").isEmpty()); + } + + @Test + public void storageConfig_roundTrip_withConfigBeanFactory() { + Config defaults = BeanDefaults.toConfig(new StorageConfig()); + StorageConfig sc = ConfigBeanFactory.create(defaults, StorageConfig.class); + Assert.assertEquals("LEVELDB", sc.getDb().getEngine()); + Assert.assertEquals(7, sc.getDbSettings().getLevelNumber()); + } + + // ── MetricsConfig nested sub-beans ─────────────────────────────────────── + + @Test + public void metricsConfig_defaultValues() { + Config cfg = BeanDefaults.toConfig(new MetricsConfig()); + + Assert.assertFalse(cfg.getBoolean("storageEnable")); + Assert.assertFalse(cfg.getBoolean("prometheus.enable")); + Assert.assertEquals(9527, cfg.getInt("prometheus.port")); + Assert.assertEquals("", cfg.getString("influxdb.ip")); + Assert.assertEquals(8086, cfg.getInt("influxdb.port")); + Assert.assertEquals("metrics", cfg.getString("influxdb.database")); + Assert.assertEquals(10, cfg.getInt("influxdb.metricsReportInterval")); + } + + @Test + public void metricsConfig_roundTrip() { + Config defaults = BeanDefaults.toConfig(new MetricsConfig()); + MetricsConfig mc = ConfigBeanFactory.create(defaults, MetricsConfig.class); + Assert.assertFalse(mc.isStorageEnable()); + Assert.assertEquals(9527, mc.getPrometheus().getPort()); + } + + // ── RateLimiterConfig ──────────────────────────────────────────────────── + + @Test + public void rateLimiterConfig_defaultValues() { + Config cfg = BeanDefaults.toConfig(new RateLimiterConfig()); + + Assert.assertEquals(50000, cfg.getInt("global.qps")); + Assert.assertEquals(10000, cfg.getInt("global.ip.qps")); + Assert.assertEquals(1000, cfg.getInt("global.api.qps")); + Assert.assertTrue(cfg.getList("http").isEmpty()); + Assert.assertTrue(cfg.getList("rpc").isEmpty()); + } + + @Test + public void rateLimiterConfig_roundTrip() { + Config defaults = BeanDefaults.toConfig(new RateLimiterConfig()); + RateLimiterConfig rl = ConfigBeanFactory.create(defaults, RateLimiterConfig.class); + Assert.assertEquals(50000, rl.getGlobal().getQps()); + Assert.assertTrue(rl.getHttp().isEmpty()); + } + + // ── CommitteeConfig ─────────────────────────────────────────────────────── + + @Test + public void committeeConfig_allZeroDefaults() { + Config cfg = BeanDefaults.toConfig(new CommitteeConfig()); + + Assert.assertEquals(0L, cfg.getLong("allowCreationOfContracts")); + Assert.assertEquals(0L, cfg.getLong("allowMultiSign")); + Assert.assertEquals(0L, cfg.getLong("allowTvmCancun")); + } + + @Test + public void committeeConfig_roundTrip() { + Config defaults = BeanDefaults.toConfig(new CommitteeConfig()); + CommitteeConfig cc = ConfigBeanFactory.create(defaults, CommitteeConfig.class); + Assert.assertEquals(0L, cc.getAllowCreationOfContracts()); + } +} diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..f3e720f4de4 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -11,20 +11,22 @@ storage { # Whether to write transaction result in transactionRetStore transHistory.switch = "on", + index.directory = "index", + index.switch = "on", # setting can improve leveldb performance .... start, deprecated for arm # node: if this will increase process fds,you may be check your ulimit if 'too many open files' error occurs # see https://github.com/tronprotocol/tips/blob/master/tip-343.md for detail # if you find block sync has lower performance, you can try this settings - # default = { - # maxOpenFiles = 100 - # } - # defaultM = { - # maxOpenFiles = 500 - # } - # defaultL = { - # maxOpenFiles = 1000 - # } + default = { + # maxOpenFiles = 100 + } + defaultM = { + # maxOpenFiles = 500 + } + defaultL = { + # maxOpenFiles = 1000 + } # setting can improve leveldb performance .... end, deprecated for arm # You can customize the configuration for each database. Otherwise, the database settings will use @@ -77,18 +79,26 @@ storage { balance.history.lookup = false - # checkpoint.version = 2 - # checkpoint.sync = true + backup = { + enable = false + propPath = "prop.properties" + bak1path = "bak1/database/" + bak2path = "bak2/database/" + frequency = 10000 + } + + checkpoint.version = 1 + checkpoint.sync = true - # the estimated number of block transactions (default 1000, min 100, max 10000). - # so the total number of cached transactions is 65536 * txCache.estimatedTransactions - # txCache.estimatedTransactions = 1000 + # the estimated number of block transactions (min 100, max 10000). + # total cached transactions = 65536 * txCache.estimatedTransactions + txCache.estimatedTransactions = 1000 # if true, transaction cache initialization will be faster. Default: false txCache.initOptimization = true # The number of blocks flushed to db in each batch during node syncing. Default: 1 - # snapshot.maxFlushCount = 1 + snapshot.maxFlushCount = 1 # data root setting, for check data, currently, only reward-vi is used. # merkleRoot = { @@ -143,7 +153,7 @@ node.metrics = { influxdb { ip = "" port = 8086 - database = "" + database = "metrics" metricsReportInterval = 10 } } @@ -151,16 +161,16 @@ node.metrics = { node { # trust node for solidity node # trustNode = "ip:port" - trustNode = "127.0.0.1:50051" + trustNode = "" # expose extension api to public or not - walletExtensionApi = true + walletExtensionApi = false listen.port = 18888 connection.timeout = 2 - fetchBlock.timeout = 200 + fetchBlock.timeout = 500 # syncFetchBatchNum = 2000 # Number of validate sign thread, default availableProcessors @@ -193,6 +203,40 @@ node { isOpenFullTcpDisconnect = false inactiveThreshold = 600 //seconds + # Maximum number of fast-forward peers. Default: 4 + maxFastForwardNum = 4 + + # Netty work thread counts; 0 = auto (availableProcessors). Default: tcp=0, udp=1 + tcpNettyWorkThreadNum = 0 + udpNettyWorkThreadNum = 1 + + # Maximum number of shielded transactions in the pending pool. Default: 10 + shieldedTransInPendingMaxCounts = 10 + + # Block cache timeout in seconds. Default: 60 + blockCacheTimeout = 60 + + # Minimum data length (bytes) to read from TCP. Default: 2048 + receiveTcpMinDataLength = 2048 + + # Maximum pending transaction pool size. Default: 2000 + maxTransactionPendingSize = 2000 + + # Timeout for pending transactions in milliseconds. Default: 60000 + pendingTransactionTimeout = 60000 + + # Required agreement count for block consensus; 0 = auto (2/3 of witness count + 1). Default: 0 + agreeNodeCount = 0 + + # Enable node-level metrics collection (prerequisite for prometheus/influxdb reporting). Default: false + metricsEnable = false + + # Channel read timeout in seconds; 0 = no timeout. Default: 0 + channel.read.timeout = 0 + + # Threads for contract protobuf validation; 0 = auto (availableProcessors). Default: 0 + validContractProto.threads = 0 + p2p { version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 } @@ -265,6 +309,9 @@ node { # The switch of the reflection service, effective for all gRPC services, used for grpcurl tool. Default: false reflectionService = false + + # Cache transactions in the RPC layer. Default: false + trxCacheEnable = false } # number of solidity thread in the FullNode. @@ -437,15 +484,17 @@ rate.limiter = { ] p2p = { - # syncBlockChain = 3.0 - # fetchInvData = 3.0 - # disconnect = 1.0 + syncBlockChain = 3.0 + fetchInvData = 3.0 + disconnect = 1.0 } # global qps, default 50000 global.qps = 50000 # IP-based global qps, default 10000 global.ip.qps = 10000 + # API-based global qps, default 1000 + global.api.qps = 1000 } @@ -837,3 +886,11 @@ event.subscribe = { ] } } + +# Whitelist of actuator class names to enable. When empty, all actuators are enabled. +# Example: ["org.tron.core.actuator.TransferActuator"] +# actuator.whitelist = [] + +# Block number from which the energy limit calculation applies. +# Note: the config key contains a legacy typo ("enery") preserved for backward compatibility. +# enery.limit.block.num = 4727890 diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index e0d9d456e9a..00acc2615d5 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -166,7 +166,7 @@ public void testInitService() { storage.put("storage.db.directory", "database"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // test default value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -195,7 +195,7 @@ public void testInitService() { storage.put("node.jsonrpc.maxSubTopics", "20"); config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // test value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -224,7 +224,7 @@ public void testInitService() { storage.put("node.jsonrpc.maxSubTopics", "1000"); config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -253,7 +253,7 @@ public void testInitService() { storage.put("node.jsonrpc.maxSubTopics", "40"); config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -273,7 +273,7 @@ public void testInitService() { storage.put("node.jsonrpc.maxSubTopics", "0"); config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // check value Args.applyConfigParams(config); Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -284,7 +284,7 @@ public void testInitService() { storage.put("node.jsonrpc.maxSubTopics", "-4"); config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(storage)) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); // check value Args.applyConfigParams(config); Assert.assertEquals(-2, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -386,7 +386,7 @@ public void testFetchBlockTimeoutClampedBelowMin() { override.put("storage.db.directory", "database"); override.put("node.fetchBlock.timeout", "50"); Config config = ConfigFactory.parseMap(override) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); Args.applyConfigParams(config); Assert.assertEquals(100, Args.getInstance().getFetchBlockTimeout()); Args.clearParam(); @@ -398,7 +398,7 @@ public void testFetchBlockTimeoutClampedAboveMax() { override.put("storage.db.directory", "database"); override.put("node.fetchBlock.timeout", "2000"); Config config = ConfigFactory.parseMap(override) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); Args.applyConfigParams(config); Assert.assertEquals(1000, Args.getInstance().getFetchBlockTimeout()); Args.clearParam(); @@ -410,7 +410,7 @@ public void testFetchBlockTimeoutInRangeUnchanged() { override.put("storage.db.directory", "database"); override.put("node.fetchBlock.timeout", "500"); Config config = ConfigFactory.parseMap(override) - .withFallback(ConfigFactory.defaultReference()); + .withFallback(ConfigFactory.empty()); Args.applyConfigParams(config); Assert.assertEquals(500, Args.getInstance().getFetchBlockTimeout()); Args.clearParam(); From afea1c9d8067eb0667bf614c6f43a62bd9897af7 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Tue, 5 May 2026 18:21:37 +0800 Subject: [PATCH 15/31] format code --- .../java/org/tron/core/config/args/Args.java | 18 ------------------ 1 file changed, 18 deletions(-) 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..cf8197a8f6b 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 @@ -2,42 +2,26 @@ import static java.lang.System.exit; import static org.tron.common.math.Maths.max; -import static org.tron.common.math.Maths.min; import static org.tron.core.Constant.ADD_PRE_FIX_BYTE_MAINNET; -import static org.tron.core.Constant.DEFAULT_PROPOSAL_EXPIRE_TIME; -import static org.tron.core.Constant.DYNAMIC_ENERGY_INCREASE_FACTOR_RANGE; -import static org.tron.core.Constant.DYNAMIC_ENERGY_MAX_FACTOR_RANGE; import static org.tron.core.Constant.ENERGY_LIMIT_IN_CONSTANT_TX; -import static org.tron.core.Constant.MAX_PROPOSAL_EXPIRE_TIME; -import static org.tron.core.Constant.MIN_PROPOSAL_EXPIRE_TIME; -import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCE_TIMEOUT_PERCENT; -import static org.tron.core.config.Parameter.ChainConstant.MAX_ACTIVE_WITNESS_NUM; -import static org.tron.core.exception.TronError.ErrCode.PARAMETER_INIT; import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterDescription; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.typesafe.config.Config; -import com.typesafe.config.ConfigObject; -import io.grpc.internal.GrpcUtil; -import io.grpc.netty.NettyServerBuilder; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -69,8 +53,6 @@ import org.tron.core.Constant; import org.tron.core.Wallet; import org.tron.core.config.Configuration; -import org.tron.core.config.Parameter.NetConstants; -import org.tron.core.config.Parameter.NodeConstant; import org.tron.core.exception.TronError; import org.tron.core.store.AccountStore; import org.tron.p2p.P2pConfig; From 9eb44ceabb5ec8a99e37aa8d1c7c077c420fd3ae Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Wed, 6 May 2026 23:52:03 +0800 Subject: [PATCH 16/31] optimize JsonRpcServlet --- .../filter/BufferedResponseWrapper.java | 10 +++- .../core/services/jsonrpc/JsonRpcServlet.java | 57 ++++++++++++------- framework/src/main/resources/config.conf | 6 +- 3 files changed, 48 insertions(+), 25 deletions(-) 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 46872b15e21..2f2522b8820 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 @@ -18,8 +18,7 @@ * 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 - * handler thread from racing with the timeout error writer. + * only forwarded to the real response via {@link #commitToResponse()}. */ public class BufferedResponseWrapper extends HttpServletResponseWrapper { @@ -28,8 +27,9 @@ public class BufferedResponseWrapper extends HttpServletResponseWrapper { private final int maxBytes; private int status = HttpServletResponse.SC_OK; private String contentType; + private boolean committed = false; @Getter - private boolean overflow = false; + private volatile boolean overflow = false; private final ServletOutputStream outputStream = new ServletOutputStream() { @Override @@ -122,6 +122,10 @@ public PrintWriter getWriter() { } public void commitToResponse() throws IOException { + if (committed) { + throw new IllegalStateException("commitToResponse() already called"); + } + committed = true; if (contentType != null) { actual.setContentType(contentType); } 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 941961cd32b..fdbb9685f57 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,7 +1,9 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; @@ -29,12 +31,13 @@ public class JsonRpcServlet extends RateLimiterServlet { private static final ObjectMapper MAPPER = new ObjectMapper(); - enum JsonRpcError { + private enum JsonRpcError { PARSE_ERROR(-32700), + INTERNAL_ERROR(-32603), EXCEED_LIMIT(-32005), RESPONSE_TOO_LARGE(-32003); - final int code; + private final int code; JsonRpcError(int code) { this.code = code; @@ -86,19 +89,25 @@ public Integer getJsonRpcCode(int httpStatusCode) { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { CommonParameter parameter = CommonParameter.getInstance(); - byte[] body; + // Transport IOException from readBody propagates as HTTP 500 (genuine IO failure). + byte[] body = readBody(req.getInputStream()); JsonNode rootNode; try { - body = readBody(req.getInputStream()); rootNode = MAPPER.readTree(body); - } catch (IOException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null); + if (rootNode == null || rootNode.isMissingNode()) { + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + return; + } + } catch (JsonProcessingException e) { + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); return; } + + boolean isBatch = rootNode.isArray(); int batchSize = parameter.getJsonRpcMaxBatchSize(); - if (rootNode.isArray() && batchSize > 0 && rootNode.size() > batchSize) { + if (isBatch && batchSize > 0 && rootNode.size() > batchSize) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, - "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null); + "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null, true); return; } @@ -108,15 +117,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rpcServer.handle(cachedReq, bufferedResp); - } catch (Exception e) { - throw new IOException("RPC execution failed", e); + } catch (RuntimeException e) { + logger.error("RPC execution failed", e); + JsonNode idNode = isBatch ? null : rootNode.get("id"); + writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", idNode, isBatch); + return; } if (bufferedResp.isOverflow()) { - JsonNode idNode = !rootNode.isArray() ? rootNode.get("id") : null; + JsonNode idNode = isBatch ? null : rootNode.get("id"); writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes", - idNode); + idNode, isBatch); return; } bufferedResp.commitToResponse(); @@ -133,18 +145,25 @@ private byte[] readBody(InputStream in) throws IOException { } private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message, - JsonNode id) throws IOException { - ObjectNode root = MAPPER.createObjectNode(); - root.put("jsonrpc", "2.0"); - ObjectNode errNode = root.putObject("error"); + JsonNode id, boolean isBatch) throws IOException { + ObjectNode errorObj = MAPPER.createObjectNode(); + errorObj.put("jsonrpc", "2.0"); + ObjectNode errNode = errorObj.putObject("error"); errNode.put("code", error.code); errNode.put("message", message); if (id != null && !id.isNull() && !id.isMissingNode()) { - root.set("id", id); + errorObj.set("id", id); + } else { + errorObj.putNull("id"); + } + byte[] bytes; + if (isBatch) { + ArrayNode arr = MAPPER.createArrayNode(); + arr.add(errorObj); + bytes = MAPPER.writeValueAsBytes(arr); } else { - root.putNull("id"); + bytes = MAPPER.writeValueAsBytes(errorObj); } - byte[] bytes = MAPPER.writeValueAsBytes(root); resp.setContentType("application/json; charset=utf-8"); 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 1ae2d30c2e5..2b27c8069e2 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -374,10 +374,10 @@ node { maxSubTopics = 1000 # Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit maxBlockFilterNum = 50000 - # Allowed batch size, default: 100, default: 100, >0 otherwise no limit + # Allowed batch size, default: 100, >0 otherwise no limit maxBatchSize = 100 - # Allowed max response byte size, default: 26214400, >0 otherwise no limit - maxResponseSize = 26214400 // 25 MB = 25 * 1024 * 1024 B + # Allowed max response byte size, default: 26214400 (25 MB), >0 otherwise no limit + maxResponseSize = 26214400 } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, From d65f06416cca7a665972454d0f9c3fc2a351ad9b Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 7 May 2026 00:23:48 +0800 Subject: [PATCH 17/31] don't invoke getInputStream and getReader in HttpServletRequestWrapper twice; add several methods of HttpServletRequestWrapper --- .../filter/BufferedResponseWrapper.java | 31 +++++++++++++++++++ .../filter/CachedBodyRequestWrapper.java | 11 +++++++ framework/src/main/resources/config.conf | 12 +++---- 3 files changed, 48 insertions(+), 6 deletions(-) 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 2f2522b8820..a4cf777d85a 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 @@ -101,11 +101,42 @@ public void setContentLengthLong(long len) { } } + @Override + public int getStatus() { + return this.status; + } + @Override public void setStatus(int sc) { this.status = sc; } + @Override + public void setHeader(String name, String value) { + if ("content-length".equalsIgnoreCase(name)) { + try { + setContentLengthLong(Long.parseLong(value)); + } catch (NumberFormatException ignored) { + // malformed value, skip overflow check + } + } else { + super.setHeader(name, value); + } + } + + @Override + public void addHeader(String name, String value) { + if ("content-length".equalsIgnoreCase(name)) { + try { + setContentLengthLong(Long.parseLong(value)); + } catch (NumberFormatException ignored) { + // malformed value, skip overflow check + } + } else { + super.addHeader(name, value); + } + } + @Override public void setContentType(String type) { this.contentType = type; 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 fcda7d34f86..1b0141b2e5b 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 @@ -15,7 +15,10 @@ */ public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { + private enum BodyAccessor { NONE, STREAM, READER } + private final byte[] body; + private BodyAccessor accessor = BodyAccessor.NONE; public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) { super(request); @@ -24,6 +27,10 @@ public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) { @Override public ServletInputStream getInputStream() { + if (accessor == BodyAccessor.READER) { + throw new IllegalStateException("getReader() has already been called on this request"); + } + accessor = BodyAccessor.STREAM; final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override @@ -54,6 +61,10 @@ public void setReadListener(ReadListener readListener) { @Override public BufferedReader getReader() { + if (accessor == BodyAccessor.STREAM) { + throw new IllegalStateException("getInputStream() has already been called on this request"); + } + accessor = BodyAccessor.READER; 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 2b27c8069e2..15149dcb6d8 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -366,17 +366,17 @@ node { # httpPBFTEnable = false # httpPBFTPort = 8565 - # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, >0 otherwise no limit + # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit maxBlockRange = 5000 - # Allowed max address count in filter request, default: 1000, >0 otherwise no limit + # Allowed max address count in filter request, default: 1000, <=0 means no limit maxAddressSize = 1000 - # The maximum number of allowed topics within a topic criteria, default: 1000, >0 otherwise no limit + # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit maxSubTopics = 1000 - # Allowed maximum number for blockFilter, default: 50000, >0 otherwise no limit + # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit maxBlockFilterNum = 50000 - # Allowed batch size, default: 100, >0 otherwise no limit + # Allowed batch size, default: 100, <=0 means no limit maxBatchSize = 100 - # Allowed max response byte size, default: 26214400 (25 MB), >0 otherwise no limit + # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit maxResponseSize = 26214400 } From 801e7ae6f0feba924d3054a415cc7762db7282a9 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 7 May 2026 10:53:07 +0800 Subject: [PATCH 18/31] add testcase of JsonRpcServlet and CachedBodyRequestWrapper --- framework/build.gradle | 1 + .../filter/BufferedResponseWrapperTest.java | 99 ++++++++ .../filter/CachedBodyRequestWrapperTest.java | 108 +++++++++ .../services/jsonrpc/JsonRpcServletTest.java | 215 ++++++++++++++++++ 4 files changed, 423 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java diff --git a/framework/build.gradle b/framework/build.gradle index d884b6a7c49..49fe02e2821 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -58,6 +58,7 @@ dependencies { } testImplementation group: 'org.springframework', name: 'spring-test', version: "${springVersion}" + testImplementation group: 'javax.portlet', name: 'portlet-api', version: '3.0.1' implementation group: 'org.zeromq', name: 'jeromq', version: '0.5.3' api project(":chainbase") api project(":protocol") 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 index cf57866e3ab..76de2d2db2e 100644 --- a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -162,4 +163,102 @@ public void statusNotForwardedBeforeCommit() throws IOException { w.commitToResponse(); assertEquals(201, mockResp.getStatus()); } + + // --- getStatus() --- + + @Test + public void getStatus_returnsBufferedValue() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.setStatus(404); + assertEquals(404, w.getStatus()); + // actual response must still be untouched + assertEquals(200, mockResp.getStatus()); + } + + @Test + public void getStatus_defaultIs200() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + assertEquals(200, w.getStatus()); + } + + // --- setHeader / addHeader for Content-Length --- + + @Test + public void setHeader_contentLength_exceedsLimit_overflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setHeader("Content-Length", "101"); + assertTrue(w.isOverflow()); + // Content-Length must NOT have been forwarded to the actual response + assertNull(mockResp.getHeader("Content-Length")); + } + + @Test + public void setHeader_contentLength_withinLimit_noOverflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setHeader("Content-Length", "100"); + assertFalse(w.isOverflow()); + } + + @Test + public void setHeader_contentLength_caseInsensitive_overflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 50); + w.setHeader("content-length", "51"); + assertTrue(w.isOverflow()); + } + + @Test + public void setHeader_contentLength_malformed_ignored() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.setHeader("Content-Length", "not-a-number"); + assertFalse(w.isOverflow()); + } + + @Test + public void setHeader_nonContentLength_passesThroughToActual() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.setHeader("X-Custom-Header", "hello"); + assertEquals("hello", mockResp.getHeader("X-Custom-Header")); + } + + @Test + public void addHeader_contentLength_exceedsLimit_overflow() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.addHeader("Content-Length", "200"); + assertTrue(w.isOverflow()); + assertNull(mockResp.getHeader("Content-Length")); + } + + @Test + public void addHeader_contentLength_malformed_ignored() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100); + w.addHeader("Content-Length", "bad"); + assertFalse(w.isOverflow()); + } + + @Test + public void addHeader_nonContentLength_passesThroughToActual() { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.addHeader("X-Trace-Id", "abc123"); + assertEquals("abc123", mockResp.getHeader("X-Trace-Id")); + } + + // --- commitToResponse idempotency --- + + @Test(expected = IllegalStateException.class) + public void commitToResponse_secondCall_throwsIllegalState() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.commitToResponse(); + w.commitToResponse(); + } + + // --- getWriter path --- + + @Test + public void writeViaWriter_commitToResponse_flushesBody() throws IOException { + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.getWriter().print("hello"); + w.getWriter().flush(); + w.commitToResponse(); + assertEquals("hello", mockResp.getContentAsString()); + } } diff --git a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java new file mode 100644 index 00000000000..85915895e8d --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java @@ -0,0 +1,108 @@ +package org.tron.core.services.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +public class CachedBodyRequestWrapperTest { + + private static final byte[] BODY = "hello world".getBytes(StandardCharsets.UTF_8); + + private static byte[] readFully(javax.servlet.ServletInputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[128]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + // --- getInputStream --- + + @Test + public void getInputStream_returnsBodyContent() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + byte[] read = readFully(w.getInputStream()); + assertEquals(new String(BODY, StandardCharsets.UTF_8), new String(read, StandardCharsets.UTF_8)); + } + + @Test + public void getInputStream_calledTwice_bothSucceed() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + w.getInputStream(); + // second call of the same accessor is allowed by the servlet spec + w.getInputStream(); + } + + // --- getReader --- + + @Test + public void getReader_returnsBodyContent() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + String line = w.getReader().readLine(); + assertEquals("hello world", line); + } + + @Test + public void getReader_calledTwice_bothSucceed() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + w.getReader(); + w.getReader(); + } + + // --- mutual exclusion --- + + @Test(expected = IllegalStateException.class) + public void getReader_afterGetInputStream_throws() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + w.getInputStream(); + w.getReader(); + } + + @Test(expected = IllegalStateException.class) + public void getInputStream_afterGetReader_throws() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + w.getReader(); + w.getInputStream(); + } + + // --- stream contract --- + + @Test + public void getInputStream_isFinished_afterFullRead() throws IOException { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + javax.servlet.ServletInputStream in = w.getInputStream(); + while (in.read() != -1) { + // drain + } + assertTrue(in.isFinished()); + } + + @Test + public void getInputStream_isReady_returnsTrue() { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); + assertTrue(w.getInputStream().isReady()); + } + + @Test + public void getInputStream_emptyBody_isFinishedImmediately() { + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), + new byte[0]); + assertTrue(w.getInputStream().isFinished()); + } + + @Test + public void getReader_usesRequestCharacterEncoding() throws IOException { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding("UTF-8"); + byte[] utf8Body = "tron".getBytes(StandardCharsets.UTF_8); + CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(req, utf8Body); + assertEquals("tron", w.getReader().readLine()); + } +} 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 new file mode 100644 index 00000000000..5d10e16d76b --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -0,0 +1,215 @@ +package org.tron.core.services.jsonrpc; + +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.googlecode.jsonrpc4j.JsonRpcServer; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.common.parameter.CommonParameter; + +public class JsonRpcServletTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private TestableServlet servlet; + private JsonRpcServer mockRpcServer; + private int savedMaxBatchSize; + private int savedMaxResponseSize; + + @Before + public void setUp() throws Exception { + servlet = new TestableServlet(); + mockRpcServer = mock(JsonRpcServer.class); + Field f = JsonRpcServlet.class.getDeclaredField("rpcServer"); + f.setAccessible(true); + f.set(servlet, mockRpcServer); + savedMaxBatchSize = CommonParameter.getInstance().jsonRpcMaxBatchSize; + savedMaxResponseSize = CommonParameter.getInstance().jsonRpcMaxResponseSize; + } + + @After + public void tearDown() { + CommonParameter.getInstance().jsonRpcMaxBatchSize = savedMaxBatchSize; + CommonParameter.getInstance().jsonRpcMaxResponseSize = savedMaxResponseSize; + } + + // --- parse error paths --- + + @Test + public void invalidJson_returnsParseError() throws Exception { + MockHttpServletResponse resp = doPost("not {{ valid json"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals(-32700, body.get("error").get("code").asInt()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertTrue(body.get("id").isNull()); + } + + @Test + public void emptyBody_returnsParseError() throws Exception { + MockHttpServletResponse resp = doPost(""); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32700, body.get("error").get("code").asInt()); + } + + // --- batch size limit --- + + @Test + public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception { + CommonParameter.getInstance().jsonRpcMaxBatchSize = 2; + MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("batch error response must be a JSON array", body.isArray()); + assertEquals(1, body.size()); + assertEquals(-32005, body.get(0).get("error").get("code").asInt()); + } + + @Test + public void batchWithinLimit_proceedsToRpcServer() throws Exception { + CommonParameter.getInstance().jsonRpcMaxBatchSize = 5; + byte[] rpcResp = "[{\"result\":\"ok\"}]".getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + r.getOutputStream().write(rpcResp); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2}]"); + assertEquals(200, resp.getStatus()); + assertArrayEquals(rpcResp, resp.getContentAsByteArray()); + } + + @Test + public void batchLimitDisabled_largeBatchAllowed() throws Exception { + CommonParameter.getInstance().jsonRpcMaxBatchSize = 0; + byte[] rpcResp = "[]".getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + r.getOutputStream().write(rpcResp); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < 500; i++) { + if (i > 0) { + sb.append(','); + } + sb.append("{}"); + } + sb.append("]"); + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + assertArrayEquals(rpcResp, resp.getContentAsByteArray()); + } + + // --- rpcServer.handle exceptions --- + + @Test + public void rpcServerThrowsRuntimeException_returnsInternalError() throws Exception { + doThrow(new RuntimeException("server exploded")).when(mockRpcServer) + .handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":42}"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals(-32603, body.get("error").get("code").asInt()); + } + + @Test + public void batchRpcServerThrows_internalErrorIsArray() throws Exception { + doThrow(new RuntimeException("boom")).when(mockRpcServer).handle((HttpServletRequest) any(), any()); + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("batch internal error must be an array", body.isArray()); + assertEquals(-32603, body.get(0).get("error").get("code").asInt()); + } + + // --- response size limit --- + + @Test + public void responseTooLarge_returnsSingleErrorObject() throws Exception { + int limit = 50; + CommonParameter.getInstance().jsonRpcMaxResponseSize = limit; + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + r.getOutputStream().write(new byte[limit + 1]); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + MockHttpServletResponse resp = doPost("{\"method\":\"eth_getLogs\",\"id\":1}"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals(-32003, body.get("error").get("code").asInt()); + } + + @Test + public void batchResponseTooLarge_returnsErrorArray() throws Exception { + int limit = 50; + CommonParameter.getInstance().jsonRpcMaxResponseSize = limit; + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + r.getOutputStream().write(new byte[limit + 1]); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("batch response-too-large must be an array", body.isArray()); + assertEquals(-32003, body.get(0).get("error").get("code").asInt()); + } + + // --- normal path --- + + @Test + public void normalRequest_commitsRpcServerResponse() throws Exception { + byte[] rpcResp = "{\"result\":\"0x1\"}".getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + r.getOutputStream().write(rpcResp); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":1}"); + assertEquals(200, resp.getStatus()); + assertArrayEquals(rpcResp, resp.getContentAsByteArray()); + } + + // --- helpers --- + + private MockHttpServletResponse doPost(String body) throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest("POST", "/jsonrpc"); + req.setContent(body.getBytes(StandardCharsets.UTF_8)); + MockHttpServletResponse resp = new MockHttpServletResponse(); + servlet.callDoPost(req, resp); + return resp; + } + + private static class TestableServlet extends JsonRpcServlet { + void callDoPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + doPost(req, resp); + } + } +} From 2bb35a860344ad0aae06b624b031ff3da10063f9 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Thu, 7 May 2026 10:56:06 +0800 Subject: [PATCH 19/31] format code of testcase --- .../core/services/filter/CachedBodyRequestWrapperTest.java | 3 ++- .../org/tron/core/services/jsonrpc/JsonRpcServletTest.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java index 85915895e8d..813b1a61bea 100644 --- a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java @@ -29,7 +29,8 @@ private static byte[] readFully(javax.servlet.ServletInputStream in) throws IOEx public void getInputStream_returnsBodyContent() throws IOException { CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY); byte[] read = readFully(w.getInputStream()); - assertEquals(new String(BODY, StandardCharsets.UTF_8), new String(read, StandardCharsets.UTF_8)); + assertEquals(new String(BODY, StandardCharsets.UTF_8), + new String(read, StandardCharsets.UTF_8)); } @Test 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 5d10e16d76b..6a6c679070f 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 @@ -137,7 +137,8 @@ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Except @Test public void batchRpcServerThrows_internalErrorIsArray() throws Exception { - doThrow(new RuntimeException("boom")).when(mockRpcServer).handle((HttpServletRequest) any(), any()); + doThrow(new RuntimeException("boom")).when(mockRpcServer) + .handle((HttpServletRequest) any(), any()); MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); assertEquals(200, resp.getStatus()); JsonNode body = MAPPER.readTree(resp.getContentAsString()); @@ -208,6 +209,7 @@ private MockHttpServletResponse doPost(String body) throws Exception { } private static class TestableServlet extends JsonRpcServlet { + void callDoPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { doPost(req, resp); } From 7de92be0f54742ca4b092e695d7ad79cc36cac7d Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 10:48:38 +0800 Subject: [PATCH 20/31] add stripNullLeaves to auto binding; simply binding not compatible varible; fix the bug of allowShieldedTransactionApi --- .../common/parameter/CommonParameter.java | 3 - .../org/tron/core/config/BeanDefaults.java | 42 +++++ .../tron/core/config/args/BlockConfig.java | 6 +- .../core/config/args/CommitteeConfig.java | 48 +---- .../tron/core/config/args/EventConfig.java | 76 +++----- .../tron/core/config/args/GenesisConfig.java | 2 +- .../tron/core/config/args/MetricsConfig.java | 2 +- .../org/tron/core/config/args/NodeConfig.java | 173 ++++++++---------- .../org/tron/core/config/args/Overlay.java | 22 --- .../core/config/args/RateLimiterConfig.java | 2 +- .../tron/core/config/args/StorageConfig.java | 20 +- .../org/tron/core/config/args/VmConfig.java | 2 +- .../tron/core/config/BeanDefaultsTest.java | 20 ++ .../core/config/args/EventConfigTest.java | 4 +- .../core/config/args/GenesisConfigTest.java | 8 +- .../tron/core/config/args/MiscConfigTest.java | 7 +- .../tron/core/config/args/NodeConfigTest.java | 6 +- .../java/org/tron/common/ParameterTest.java | 1 - .../tron/core/config/args/OverlayTest.java | 40 ---- 19 files changed, 195 insertions(+), 289 deletions(-) delete mode 100644 common/src/main/java/org/tron/core/config/args/Overlay.java delete mode 100644 framework/src/test/java/org/tron/core/config/args/OverlayTest.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 e7957c917e2..c2b233d6533 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -15,7 +15,6 @@ import org.tron.common.logsfilter.FilterQuery; import org.tron.common.setting.RocksDbSettings; import org.tron.core.Constant; -import org.tron.core.config.args.Overlay; import org.tron.core.config.args.SeedNode; import org.tron.core.config.args.Storage; import org.tron.p2p.P2pConfig; @@ -415,8 +414,6 @@ public class CommonParameter { @Getter public Storage storage; @Getter - public Overlay overlay; - @Getter public SeedNode seedNode; @Getter public EventPluginConfig eventPluginConfig; diff --git a/common/src/main/java/org/tron/core/config/BeanDefaults.java b/common/src/main/java/org/tron/core/config/BeanDefaults.java index 4c279cee7c2..1e54bac5724 100644 --- a/common/src/main/java/org/tron/core/config/BeanDefaults.java +++ b/common/src/main/java/org/tron/core/config/BeanDefaults.java @@ -2,6 +2,9 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; @@ -43,6 +46,45 @@ public static Config toConfig(Object bean) { return ConfigFactory.parseMap(toMap(bean)); } + /** + * Returns a copy of {@code config} with all null-valued leaf paths removed. + * Call this on a user-supplied config section before {@link Config#withFallback} + * so that HOCON {@code null} entries in legacy configs do not shadow bean defaults. + * + *

Uses {@link ConfigObject#entrySet()} (not {@link Config#entrySet()}) because + * the latter silently excludes null values, making them impossible to detect. + */ + public static Config stripNullLeaves(Config config) { + return stripNullObject(config.root()).toConfig(); + } + + /** + * Returns a copy of {@code config} where the value at {@code fromKey} is moved to + * {@code toKey}, leaving the original key absent. If {@code fromKey} is absent, the + * config is returned unchanged. Use this in {@code fromConfig()} to bridge config keys + * that violate JavaBean naming (e.g. {@code pBFTExpireNum} → {@code PBFTExpireNum}) so + * that {@code ConfigBeanFactory} finds the value under the key it derives from the setter. + */ + public static Config remapKey(Config config, String fromKey, String toKey) { + if (!config.hasPath(fromKey)) { + return config; + } + return config.withValue(toKey, config.getValue(fromKey)).withoutPath(fromKey); + } + + private static ConfigObject stripNullObject(ConfigObject obj) { + ConfigObject result = obj; + for (Map.Entry entry : obj.entrySet()) { + ConfigValue v = entry.getValue(); + if (v.valueType() == ConfigValueType.NULL) { + result = result.withoutKey(entry.getKey()); + } else if (v.valueType() == ConfigValueType.OBJECT) { + result = result.withValue(entry.getKey(), stripNullObject((ConfigObject) v)); + } + } + return result; + } + private static Map toMap(Object bean) { Map map = new LinkedHashMap<>(); try { diff --git a/common/src/main/java/org/tron/core/config/args/BlockConfig.java b/common/src/main/java/org/tron/core/config/args/BlockConfig.java index 6ade2b00068..567446e1003 100644 --- a/common/src/main/java/org/tron/core/config/args/BlockConfig.java +++ b/common/src/main/java/org/tron/core/config/args/BlockConfig.java @@ -7,10 +7,10 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; -import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.tron.core.config.BeanDefaults; import org.tron.core.exception.TronError; /** @@ -22,7 +22,7 @@ public class BlockConfig { private boolean needSyncCheck = false; - private long maintenanceTimeInterval = 21600000L; + private long maintenanceTimeInterval = 6 * 3600 * 1000L; // 6 hours private long proposalExpireTime = DEFAULT_PROPOSAL_EXPIRE_TIME; private int checkFrozenTime = 1; @@ -39,7 +39,7 @@ public static BlockConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new BlockConfig()); Config blockSection = config.hasPath("block") - ? config.getConfig("block").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("block")).withFallback(defaults) : defaults; BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class); blockConfig.postProcess(); diff --git a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java index c2ad56f227c..5ef6e958957 100644 --- a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; @@ -36,29 +37,8 @@ public class CommitteeConfig { private long allowProtoFilterNum = 0; private long allowAccountStateRoot = 0; private long changedDelegation = 0; - // NON-STANDARD NAMING: "allowPBFT" and "pBFTExpireNum" in config.conf contain - // consecutive uppercase letters ("PBFT"), which violates JavaBean naming convention. - // ConfigBeanFactory derives config keys from setter names using JavaBean rules: - // setPBFTExpireNum -> property "PBFTExpireNum" (capital P, per JavaBean spec) - // but config.conf uses "pBFTExpireNum" (lowercase p) -> mismatch -> binding fails. - // - // These two fields are excluded from auto-binding and handled manually in fromConfig(). - // TODO: Rename config keys to standard camelCase (allowPbft, pbftExpireNum) when - // PBFT feature is enabled and a breaking config change is acceptable. - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private long allowPBFT = 0; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private long pBFTExpireNum = 20; - - // Only getters are exposed. No public setters — ConfigBeanFactory scans public - // setters via reflection and would derive key "PBFTExpireNum" / "AllowPBFT" - // (JavaBean uppercase rule), which does not match config keys "pBFTExpireNum" - // / "allowPBFT" and would throw. Values are assigned to fields directly in - // fromConfig() below. - public long getAllowPBFT() { return allowPBFT; } - public long getPBFTExpireNum() { return pBFTExpireNum; } private long allowTvmFreeze = 0; private long allowTvmVote = 0; private long allowTvmLondon = 0; @@ -88,27 +68,17 @@ public class CommitteeConfig { // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig - /** - * Create CommitteeConfig from the "committee" section of the application config. - * - * Note: allowPBFT and pBFTExpireNum have non-standard JavaBean naming (consecutive - * uppercase letters) which causes ConfigBeanFactory key mismatch. These two fields - * are excluded from automatic binding and handled manually after. - */ - private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum"; - private static final String ALLOW_PBFT_KEY = "allowPBFT"; - public static CommitteeConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new CommitteeConfig()); - Config section = config.hasPath("committee") - ? config.getConfig("committee").withFallback(defaults) - : defaults; + Config userSection = config.hasPath("committee") + ? BeanDefaults.stripNullLeaves(config.getConfig("committee")) + : ConfigFactory.empty(); + // pBFTExpireNum: config key uses lowercase-p prefix, but setPBFTExpireNum causes + // Introspector to derive "PBFTExpireNum" (consecutive uppercase prevents decapitalization). + // Remap so ConfigBeanFactory finds it under the expected key. + userSection = BeanDefaults.remapKey(userSection, "pBFTExpireNum", "PBFTExpireNum"); + Config section = userSection.withFallback(defaults); CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class); - // Ensure the manually-named fields get the right values from the original keys - cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0; - cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY) - ? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20; - cc.postProcess(); return cc; } diff --git a/common/src/main/java/org/tron/core/config/args/EventConfig.java b/common/src/main/java/org/tron/core/config/args/EventConfig.java index 42c03a04b8f..0dd8750e148 100644 --- a/common/src/main/java/org/tron/core/config/args/EventConfig.java +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -3,12 +3,12 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigFactory; -import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.tron.core.config.BeanDefaults; /** * Event subscribe configuration bean. @@ -26,11 +26,9 @@ public class EventConfig { private String server = ""; private String dbconfig = ""; private boolean contractParse = true; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) + // Config key is "native" (Java reserved word); remapped to this field in fromConfig(). private NativeConfig nativeQueue = new NativeConfig(); - public NativeConfig getNativeQueue() { return nativeQueue; } // Topics list has optional fields (ethCompatible, redundancy, solidified) that // not all items have. ConfigBeanFactory requires all bean fields to exist in config. // Excluded from auto-binding, read manually in fromConfig(). @@ -38,12 +36,16 @@ public class EventConfig { @Setter(lombok.AccessLevel.NONE) private List topics = new ArrayList<>(); - public List getTopics() { return topics; } + public List getTopics() { + return topics; + } + private FilterConfig filter = new FilterConfig(); @Getter @Setter public static class NativeConfig { + private boolean useNativeQueue = true; private int bindport = 5555; private int sendqueuelength = 1000; @@ -52,6 +54,7 @@ public static class NativeConfig { @Getter @Setter public static class TopicConfig { + private String triggerName = ""; private boolean enable = false; private String topic = ""; @@ -63,6 +66,7 @@ public static class TopicConfig { @Getter @Setter public static class FilterConfig { + private String fromblock = ""; private String toblock = ""; private List contractAddress = new ArrayList<>(); @@ -76,61 +80,29 @@ public static class FilterConfig { * "nativeQueue" but config key is "native". We handle this manually after binding. */ public static EventConfig fromConfig(Config config) { - // BeanDefaults covers enable/version/startSyncBlockNum/path/server/dbconfig/ - // contractParse/filter. nativeQueue and topics are excluded (no public setter). Config defaults = BeanDefaults.toConfig(new EventConfig()); - Config section = config.hasPath("event.subscribe") - ? config.getConfig("event.subscribe") + Config userSection = config.hasPath("event.subscribe") + ? BeanDefaults.stripNullLeaves(config.getConfig("event.subscribe")) : ConfigFactory.empty(); - // "native" is a Java reserved word, "topics" has optional fields per item — - // strip both before binding, read manually - String nativeKey = "native"; - String topicsKey = "topics"; - Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey) + // "native" is a Java reserved word — remap to the field name so ConfigBeanFactory + // auto-binds it as NativeConfig nativeQueue. topics has optional fields per item + // so it is excluded from auto-binding and populated manually below. + Config bindable = BeanDefaults.remapKey(userSection, "native", "nativeQueue") + .withoutPath("topics") .withoutPath("topicDefaults") .withFallback(defaults); EventConfig ec = ConfigBeanFactory.create(bindable, EventConfig.class); - // manually bind "native" sub-section - Config nativeSection = section.hasPath(nativeKey) - ? section.getConfig(nativeKey) : ConfigFactory.empty(); - ec.nativeQueue = new NativeConfig(); - if (nativeSection.hasPath("useNativeQueue")) { - ec.nativeQueue.useNativeQueue = nativeSection.getBoolean("useNativeQueue"); - } - if (nativeSection.hasPath("bindport")) { - ec.nativeQueue.bindport = nativeSection.getInt("bindport"); - } - if (nativeSection.hasPath("sendqueuelength")) { - ec.nativeQueue.sendqueuelength = nativeSection.getInt("sendqueuelength"); - } - - // manually bind topics — each item may have optional fields - if (section.hasPath(topicsKey)) { + // topics: apply per-item BeanDefaults so optional fields (solidified, ethCompatible, + // redundancy) don't require every item to declare them explicitly. + if (userSection.hasPath("topics")) { + Config topicDefaults = BeanDefaults.toConfig(new TopicConfig()); ec.topics = new ArrayList<>(); - for (com.typesafe.config.ConfigObject obj : section.getObjectList(topicsKey)) { - Config tc = obj.toConfig(); - TopicConfig topic = new TopicConfig(); - if (tc.hasPath("triggerName")) { - topic.triggerName = tc.getString("triggerName"); - } - if (tc.hasPath("enable")) { - topic.enable = tc.getBoolean("enable"); - } - if (tc.hasPath("topic")) { - topic.topic = tc.getString("topic"); - } - if (tc.hasPath("solidified")) { - topic.solidified = tc.getBoolean("solidified"); - } - if (tc.hasPath("ethCompatible")) { - topic.ethCompatible = tc.getBoolean("ethCompatible"); - } - if (tc.hasPath("redundancy")) { - topic.redundancy = tc.getBoolean("redundancy"); - } - ec.topics.add(topic); + for (com.typesafe.config.ConfigObject obj : userSection.getObjectList("topics")) { + ec.topics.add(ConfigBeanFactory.create( + BeanDefaults.stripNullLeaves(obj.toConfig()).withFallback(topicDefaults), + TopicConfig.class)); } } diff --git a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java index 74d4338bbb5..645fc238ee0 100644 --- a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java +++ b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java @@ -45,7 +45,7 @@ public static class WitnessConfig { public static GenesisConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new GenesisConfig()); Config section = config.hasPath("genesis.block") - ? config.getConfig("genesis.block").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("genesis.block")).withFallback(defaults) : defaults; return ConfigBeanFactory.create(section, GenesisConfig.class); } diff --git a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java index 98d5c0a36d7..da0404d516d 100644 --- a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java +++ b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java @@ -39,7 +39,7 @@ public static class InfluxDbConfig { public static MetricsConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new MetricsConfig()); Config section = config.hasPath("node.metrics") - ? config.getConfig("node.metrics").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("node.metrics")).withFallback(defaults) : defaults; return ConfigBeanFactory.create(section, MetricsConfig.class); } 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 02b7eee87b6..2bd912db493 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 @@ -5,12 +5,12 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigFactory; -import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.tron.core.config.BeanDefaults; // Node configuration bean for the "node" section of config.conf. // ConfigBeanFactory auto-binds all fields including sub-beans, dot-notation keys, @@ -42,7 +42,9 @@ public class NodeConfig { @Setter(lombok.AccessLevel.NONE) private boolean isOpenFullTcpDisconnect = false; - public boolean isOpenFullTcpDisconnect() { return isOpenFullTcpDisconnect; } + public boolean isOpenFullTcpDisconnect() { + return isOpenFullTcpDisconnect; + } // node.discovery.* — HOCON merges into node { discovery { ... } }, auto-bound private DiscoveryConfig discovery = new DiscoveryConfig(); @@ -59,12 +61,30 @@ public class NodeConfig { @Setter(lombok.AccessLevel.NONE) private long shutdownBlockCount = -1; - public boolean isDiscoveryEnable() { return discovery.isEnable(); } - public boolean isDiscoveryPersist() { return discovery.isPersist(); } - public String getDiscoveryExternalIp() { return discovery.getExternal().getIp(); } - public String getShutdownBlockTime() { return shutdownBlockTime; } - public long getShutdownBlockHeight() { return shutdownBlockHeight; } - public long getShutdownBlockCount() { return shutdownBlockCount; } + public boolean isDiscoveryEnable() { + return discovery.isEnable(); + } + + public boolean isDiscoveryPersist() { + return discovery.isPersist(); + } + + public String getDiscoveryExternalIp() { + return discovery.getExternal().getIp(); + } + + public String getShutdownBlockTime() { + return shutdownBlockTime; + } + + public long getShutdownBlockHeight() { + return shutdownBlockHeight; + } + + public long getShutdownBlockCount() { + return shutdownBlockCount; + } + private int inactiveThreshold = 600; private boolean metricsEnable = false; private int blockProducedTimeOut = 50; @@ -101,12 +121,29 @@ public class NodeConfig { private SolidityConfig solidity = new SolidityConfig(); // Convenience getters for backward compatibility with applyNodeConfig - public int getListenPort() { return listen.getPort(); } - public int getConnectionTimeout() { return connection.getTimeout(); } - public int getFetchBlockTimeout() { return fetchBlock.getTimeout(); } - public int getSolidityThreads() { return solidity.getThreads(); } - public int getChannelReadTimeout() { return channel.getRead().getTimeout(); } - public int getValidContractProtoThreads() { return validContractProto.getThreads(); } + public int getListenPort() { + return listen.getPort(); + } + + public int getConnectionTimeout() { + return connection.getTimeout(); + } + + public int getFetchBlockTimeout() { + return fetchBlock.getTimeout(); + } + + public int getSolidityThreads() { + return solidity.getThreads(); + } + + public int getChannelReadTimeout() { + return channel.getRead().getTimeout(); + } + + public int getValidContractProtoThreads() { + return validContractProto.getThreads(); + } // ---- List fields (manually read) ---- private List active = new ArrayList<>(); @@ -199,69 +236,30 @@ public static class HttpConfig { private int fullNodePort = 8090; private boolean solidityEnable = true; private int solidityPort = 8091; - // PBFT fields — handled manually (same naming issue as CommitteeConfig) - // Default must match CommonParameter.pBFTHttpEnable = true - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) + // pBFTEnable/pBFTPort: fromConfig() remaps "pBFTEnable"→"PBFTEnable" so + // ConfigBeanFactory finds these under the JavaBean-derived key name. private boolean pBFTEnable = true; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int pBFTPort = 8092; - - public boolean isPBFTEnable() { - return pBFTEnable; - } - - public void setPBFTEnable(boolean v) { - this.pBFTEnable = v; - } - - public int getPBFTPort() { - return pBFTPort; - } - - public void setPBFTPort(int v) { - this.pBFTPort = v; - } } @Getter @Setter public static class RpcConfig { + private boolean enable = true; private int port = 50051; private boolean solidityEnable = true; private int solidityPort = 50061; - // PBFT fields — handled manually - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) + // pBFTEnable/pBFTPort: remapped in NodeConfig.fromConfig() (same reason as HttpConfig). private boolean pBFTEnable = true; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int pBFTPort = 50071; - public boolean isPBFTEnable() { - return pBFTEnable; - } - - public void setPBFTEnable(boolean v) { - this.pBFTEnable = v; - } - - public int getPBFTPort() { - return pBFTPort; - } - - public void setPBFTPort(int v) { - this.pBFTPort = v; - } - private int thread = 0; - private int maxConcurrentCallsPerConnection = 2147483647; - private int flowControlWindow = 1048576; + private int maxConcurrentCallsPerConnection = Integer.MAX_VALUE; + private int flowControlWindow = 1024 * 1024; private long maxConnectionIdleInMillis = Long.MAX_VALUE; private long maxConnectionAgeInMillis = Long.MAX_VALUE; - private int maxMessageSize = 4194304; + private int maxMessageSize = 4 * 1024 * 1024; private int maxHeaderListSize = 8192; private int maxRstStream = 0; private int secondsPerWindow = 0; @@ -273,34 +271,16 @@ public void setPBFTPort(int v) { @Getter @Setter public static class JsonRpcConfig { + private boolean httpFullNodeEnable = false; private int httpFullNodePort = 8545; private boolean httpSolidityEnable = false; private int httpSolidityPort = 8555; - // PBFT fields — handled manually - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) + // httpPBFTEnable/httpPBFTPort: setHttpPBFTEnable → property "httpPBFTEnable" — matches + // config key directly, no remapping needed. private boolean httpPBFTEnable = false; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int httpPBFTPort = 8565; - public boolean isHttpPBFTEnable() { - return httpPBFTEnable; - } - - public void setHttpPBFTEnable(boolean v) { - this.httpPBFTEnable = v; - } - - public int getHttpPBFTPort() { - return httpPBFTPort; - } - - public void setHttpPBFTPort(int v) { - this.httpPBFTPort = v; - } - private int maxBlockRange = 5000; private int maxSubTopics = 1000; private int maxBlockFilterNum = 50000; @@ -352,20 +332,27 @@ public static class DnsConfig { * solidity.threads) become nested HOCON objects and cannot be auto-bound to flat * Java fields. They are read manually after ConfigBeanFactory binding. * - *

PBFT-named fields in http, rpc, and jsonrpc sub-beans have the same JavaBean - * naming issue as CommitteeConfig and are patched manually. + *

pBFT-prefixed fields in http and rpc sub-beans are remapped before binding + * (pBFTEnable → PBFTEnable) because consecutive uppercase letters prevent + * Introspector from decapitalizing the JavaBean property name. + * jsonrpc.httpPBFT* binds directly (httpPBFTEnable → property "httpPBFTEnable" ✓). * - *

List fields (active, passive, fastForward, disabledApi) are read manually - * since ConfigBeanFactory expects typed bean lists, not string lists. + *

List fields (active, passive, fastForward, disabledApi) auto-bind via + * ConfigBeanFactory's List<String> support. */ public static NodeConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new NodeConfig()); - Config section = config.hasPath("node") - ? config.getConfig("node").withFallback(defaults) - : defaults; - // Auto-bind all fields and sub-beans. ConfigBeanFactory fails fast with a - // descriptive path on any `= null` value — external configs that use the - // HOCON null keyword should fix their config rather than rely on silent coercion. + Config userSection = config.hasPath("node") + ? BeanDefaults.stripNullLeaves(config.getConfig("node")) + : ConfigFactory.empty(); + // pBFTEnable/pBFTPort: config uses lowercase-p prefix; JavaBean derives "PBFTEnable" + // (consecutive uppercase prevents Introspector from decapitalizing). Remap so + // ConfigBeanFactory binds the user value instead of silently using the default. + userSection = BeanDefaults.remapKey(userSection, "http.pBFTEnable", "http.PBFTEnable"); + userSection = BeanDefaults.remapKey(userSection, "http.pBFTPort", "http.PBFTPort"); + userSection = BeanDefaults.remapKey(userSection, "rpc.pBFTEnable", "rpc.PBFTEnable"); + userSection = BeanDefaults.remapKey(userSection, "rpc.pBFTPort", "rpc.PBFTPort"); + Config section = userSection.withFallback(defaults); NodeConfig nc = ConfigBeanFactory.create(section, NodeConfig.class); // isOpenFullTcpDisconnect: boolean "is" prefix breaks JavaBean pairing @@ -390,7 +377,8 @@ public static NodeConfig fromConfig(Config config) { // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. // BeanDefaults does not emit this legacy key, so hasPath here reliably means the user // set it in their config. When present, it overrides the modern key. - if (section.hasPath("fullNodeAllowShieldedTransaction")) { + if (!section.hasPath("allowShieldedTransactionApi") && + section.hasPath("fullNodeAllowShieldedTransaction")) { nc.allowShieldedTransactionApi = section.getBoolean("fullNodeAllowShieldedTransaction"); logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " + "Please use [node.allowShieldedTransactionApi] instead."); @@ -403,7 +391,6 @@ public static NodeConfig fromConfig(Config config) { nc.shutdownBlockCount = config.hasPath("node.shutdown.BlockCount") ? config.getLong("node.shutdown.BlockCount") : -1; - nc.postProcess(); return nc; } diff --git a/common/src/main/java/org/tron/core/config/args/Overlay.java b/common/src/main/java/org/tron/core/config/args/Overlay.java deleted file mode 100644 index bdaa40724c7..00000000000 --- a/common/src/main/java/org/tron/core/config/args/Overlay.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.tron.core.config.args; - -import lombok.Getter; -import org.apache.commons.lang3.Range; - -public class Overlay { - - @Getter - private int port; - - /** - * Monitor port number. - */ - public void setPort(final int port) { - Range range = Range.between(0, 65535); - if (!range.contains(port)) { - throw new IllegalArgumentException("Port(" + port + ") must in [0, 65535]"); - } - - this.port = port; - } -} diff --git a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java index 8fda4fba49c..ed92ed600bd 100644 --- a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -70,7 +70,7 @@ public static class RpcRateLimitItem { public static RateLimiterConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new RateLimiterConfig()); Config section = config.hasPath("rate.limiter") - ? config.getConfig("rate.limiter").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("rate.limiter")).withFallback(defaults) : defaults; return ConfigBeanFactory.create(section, RateLimiterConfig.class); } diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index ad8a41d35d2..45def5d8fe2 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -33,38 +33,20 @@ public class StorageConfig { private TxCacheConfig txCache = new TxCacheConfig(); private List properties = new ArrayList<>(); - // merkleRoot is a nested object (e.g. { reward-vi = "hash..." }) not a string. - // Excluded from auto-binding, handled by Storage class directly. - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private Object merkleRoot; - // Raw storage config sub-tree, kept for setCacheStrategies/setDbRoots which // have dynamic keys that ConfigBeanFactory cannot bind. - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private Config rawStorageConfig; - public Config getRawStorageConfig() { - return rawStorageConfig; - } - // LevelDB per-database option overrides (default, defaultM, defaultL). // Excluded from auto-binding: optional partial overrides that ConfigBeanFactory cannot handle. - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultDbOption; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultMDbOption; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultLDbOption; - public DbOptionOverride getDefaultDbOption() { return defaultDbOption; } - public DbOptionOverride getDefaultMDbOption() { return defaultMDbOption; } - public DbOptionOverride getDefaultLDbOption() { return defaultLDbOption; } - @Getter @Setter public static class DbConfig { @@ -212,7 +194,7 @@ public static StorageConfig fromConfig(Config config) { // readDbOption() uses hasPath() on the merged section, so user-set optional keys // (default, defaultM, defaultL) are still detected correctly. Config section = config.hasPath("storage") - ? config.getConfig("storage").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("storage")).withFallback(defaults) : defaults; StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); sc.rawStorageConfig = section; diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index a4b219f8726..c0136d06594 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -32,7 +32,7 @@ public class VmConfig { public static VmConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new VmConfig()); Config vmSection = config.hasPath("vm") - ? config.getConfig("vm").withFallback(defaults) + ? BeanDefaults.stripNullLeaves(config.getConfig("vm")).withFallback(defaults) : defaults; VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); vmConfig.postProcess(); diff --git a/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java index c4170d580c1..88107484b01 100644 --- a/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java +++ b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java @@ -213,4 +213,24 @@ public void committeeConfig_roundTrip() { CommitteeConfig cc = ConfigBeanFactory.create(defaults, CommitteeConfig.class); Assert.assertEquals(0L, cc.getAllowCreationOfContracts()); } + + @Test + public void stripNullLeaves_removesNullPaths() { + Config cfg = ConfigFactory.parseString("a = null\nb = 1\nc.d = null\nc.e = 2"); + Config stripped = BeanDefaults.stripNullLeaves(cfg); + Assert.assertFalse(stripped.hasPath("a")); + Assert.assertTrue(stripped.hasPath("b")); + Assert.assertFalse(stripped.hasPath("c.d")); + Assert.assertTrue(stripped.hasPath("c.e")); + } + + @Test + public void nodeConfig_fromConfig_toleratesNullExternalIp() { + // Legacy configs used "node.discovery.external.ip = null" — must not throw. + Config cfg = ConfigFactory.parseString( + "node { discovery { external { ip = null } } }"); + NodeConfig nc = NodeConfig.fromConfig(cfg); + Assert.assertNotNull(nc); + Assert.assertEquals("", nc.getDiscoveryExternalIp()); + } } diff --git a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java index 361d9f48581..1aaca42f6fc 100644 --- a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -22,11 +22,11 @@ private static Config withRef() { public void testDefaults() { Config empty = withRef(); EventConfig ec = EventConfig.fromConfig(empty); - // reference.conf has event.subscribe with enable=false, topics with 7 entries + // BeanDefaults provides scalar defaults; topics list is empty by default (user must configure) assertFalse(ec.isEnable()); assertEquals(0, ec.getVersion()); assertEquals("", ec.getPath()); - assertFalse(ec.getTopics().isEmpty()); // reference.conf has default topic entries + assertTrue(ec.getTopics().isEmpty()); } @Test diff --git a/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java index 5e653a79b7f..4f3e8829ade 100644 --- a/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java @@ -22,10 +22,10 @@ private static Config withRef() { public void testDefaults() { Config empty = withRef(); GenesisConfig gc = GenesisConfig.fromConfig(empty); - // reference.conf has genesis.block with timestamp, parentHash, assets, witnesses - assertEquals("0", gc.getTimestamp()); - assertFalse(gc.getAssets().isEmpty()); // reference.conf has seed accounts - assertFalse(gc.getWitnesses().isEmpty()); // reference.conf has seed witnesses + // BeanDefaults: timestamp/parentHash default to "", assets/witnesses to empty lists + assertEquals("", gc.getTimestamp()); + assertTrue(gc.getAssets().isEmpty()); + assertTrue(gc.getWitnesses().isEmpty()); } @Test diff --git a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java index ed369d6c35f..de06b31ac4b 100644 --- a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java @@ -28,10 +28,9 @@ public void testDefaults() { assertEquals("solid", mc.getTrxReferenceBlock()); assertEquals(Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME, mc.getTrxExpirationTimeInMilliseconds()); - // reference.conf has crypto.engine = "eckey" (lowercase) - assertEquals("eckey", mc.getCryptoEngine()); - // reference.conf has seed.node.ip.list with actual IPs - assertFalse(mc.getSeedNodeIpList().isEmpty()); + // BeanDefaults: cryptoEngine defaults to Constant.ECKey_ENGINE; seedNodeIpList defaults to empty + assertEquals(Constant.ECKey_ENGINE, mc.getCryptoEngine()); + assertTrue(mc.getSeedNodeIpList().isEmpty()); assertTrue(mc.getActuatorWhitelist().isEmpty()); } diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index fb22029262e..39500a12197 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -105,10 +105,10 @@ public void testRpcDefaultsFromReference() { NodeConfig.RpcConfig rpc = nc.getRpc(); // reference.conf provides actual final defaults, no sentinel conversion needed - assertEquals(2147483647, rpc.getMaxConcurrentCallsPerConnection()); + assertEquals(Integer.MAX_VALUE, rpc.getMaxConcurrentCallsPerConnection()); assertEquals(1048576, rpc.getFlowControlWindow()); - assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis()); - assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis()); + assertEquals(Long.MAX_VALUE, rpc.getMaxConnectionIdleInMillis()); + assertEquals(Long.MAX_VALUE, rpc.getMaxConnectionAgeInMillis()); assertEquals(4194304, rpc.getMaxMessageSize()); assertEquals(8192, rpc.getMaxHeaderListSize()); assertEquals(1, rpc.getMinEffectiveConnection()); diff --git a/framework/src/test/java/org/tron/common/ParameterTest.java b/framework/src/test/java/org/tron/common/ParameterTest.java index d5dbced87fe..81a2e3fcdcd 100644 --- a/framework/src/test/java/org/tron/common/ParameterTest.java +++ b/framework/src/test/java/org/tron/common/ParameterTest.java @@ -226,7 +226,6 @@ public void testCommonParameter() { assertEquals(1000, parameter.getRateLimiterGlobalQps()); parameter.setRateLimiterGlobalIpQps(100); assertEquals(100, parameter.getRateLimiterGlobalIpQps()); - assertNull(parameter.getOverlay()); assertNull(parameter.getEventPluginConfig()); assertNull(parameter.getEventFilter()); parameter.setCryptoEngine(ECKey_ENGINE); diff --git a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java b/framework/src/test/java/org/tron/core/config/args/OverlayTest.java deleted file mode 100644 index 1b7045c5b21..00000000000 --- a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * java-tron is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * java-tron is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.tron.core.config.args; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class OverlayTest { - - private Overlay overlay = new Overlay(); - - @Before - public void setOverlay() { - overlay.setPort(8080); - } - - @Test(expected = IllegalArgumentException.class) - public void whenSetOutOfBoundsPort() { - overlay.setPort(-1); - } - - @Test - public void getOverlay() { - Assert.assertEquals(8080, overlay.getPort()); - } -} From 650e8e1dc4d347a09ccb76074c891860888b8856 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 13:50:06 +0800 Subject: [PATCH 21/31] add userSection; resolve the priority of allowShieldedTransactionApi --- .../java/org/tron/core/config/args/BlockConfig.java | 10 ++++++---- .../org/tron/core/config/args/GenesisConfig.java | 8 +++++--- .../org/tron/core/config/args/MetricsConfig.java | 8 +++++--- .../java/org/tron/core/config/args/NodeConfig.java | 6 +++--- .../tron/core/config/args/RateLimiterConfig.java | 8 +++++--- .../org/tron/core/config/args/StorageConfig.java | 13 ++++++++----- .../java/org/tron/core/config/args/VmConfig.java | 10 ++++++---- .../org/tron/core/config/args/NodeConfigTest.java | 6 +++--- framework/src/main/resources/config.conf | 5 +++-- 9 files changed, 44 insertions(+), 30 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/BlockConfig.java b/common/src/main/java/org/tron/core/config/args/BlockConfig.java index 567446e1003..a0e187f1b5e 100644 --- a/common/src/main/java/org/tron/core/config/args/BlockConfig.java +++ b/common/src/main/java/org/tron/core/config/args/BlockConfig.java @@ -7,6 +7,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -38,10 +39,11 @@ public static BlockConfig fromConfig(Config config) { } Config defaults = BeanDefaults.toConfig(new BlockConfig()); - Config blockSection = config.hasPath("block") - ? BeanDefaults.stripNullLeaves(config.getConfig("block")).withFallback(defaults) - : defaults; - BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class); + Config userSection = config.hasPath("block") + ? BeanDefaults.stripNullLeaves(config.getConfig("block")) + : ConfigFactory.empty(); + Config section = userSection.withFallback(defaults); + BlockConfig blockConfig = ConfigBeanFactory.create(section, BlockConfig.class); blockConfig.postProcess(); return blockConfig; } diff --git a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java index 645fc238ee0..6d87ede74f7 100644 --- a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java +++ b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; @@ -44,9 +45,10 @@ public static class WitnessConfig { public static GenesisConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new GenesisConfig()); - Config section = config.hasPath("genesis.block") - ? BeanDefaults.stripNullLeaves(config.getConfig("genesis.block")).withFallback(defaults) - : defaults; + Config userSection = config.hasPath("genesis.block") + ? BeanDefaults.stripNullLeaves(config.getConfig("genesis.block")) + : ConfigFactory.empty(); + Config section = userSection.withFallback(defaults); return ConfigBeanFactory.create(section, GenesisConfig.class); } } diff --git a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java index da0404d516d..a2462631232 100644 --- a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java +++ b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; @@ -38,9 +39,10 @@ public static class InfluxDbConfig { public static MetricsConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new MetricsConfig()); - Config section = config.hasPath("node.metrics") - ? BeanDefaults.stripNullLeaves(config.getConfig("node.metrics")).withFallback(defaults) - : defaults; + Config userSection = config.hasPath("node.metrics") + ? BeanDefaults.stripNullLeaves(config.getConfig("node.metrics")) + : ConfigFactory.empty(); + Config section = userSection.withFallback(defaults); return ConfigBeanFactory.create(section, MetricsConfig.class); } } 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 2bd912db493..1e27e5234b8 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 @@ -374,11 +374,11 @@ public static NodeConfig fromConfig(Config config) { nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); } - // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. + // Legacy key fallback: node.allowShieldedTransactionApi -> fullNodeAllowShieldedTransaction. // BeanDefaults does not emit this legacy key, so hasPath here reliably means the user // set it in their config. When present, it overrides the modern key. - if (!section.hasPath("allowShieldedTransactionApi") && - section.hasPath("fullNodeAllowShieldedTransaction")) { + if (!userSection.hasPath("allowShieldedTransactionApi") && + userSection.hasPath("fullNodeAllowShieldedTransaction")) { nc.allowShieldedTransactionApi = section.getBoolean("fullNodeAllowShieldedTransaction"); logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " + "Please use [node.allowShieldedTransactionApi] instead."); diff --git a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java index ed92ed600bd..33f42569154 100644 --- a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; @@ -69,9 +70,10 @@ public static class RpcRateLimitItem { public static RateLimiterConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new RateLimiterConfig()); - Config section = config.hasPath("rate.limiter") - ? BeanDefaults.stripNullLeaves(config.getConfig("rate.limiter")).withFallback(defaults) - : defaults; + Config userSection = config.hasPath("rate.limiter") + ? BeanDefaults.stripNullLeaves(config.getConfig("rate.limiter")) + : ConfigFactory.empty(); + Config section = userSection.withFallback(defaults); return ConfigBeanFactory.create(section, RateLimiterConfig.class); } } diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 45def5d8fe2..c12a92dcbc6 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -2,8 +2,9 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; -import org.tron.core.config.BeanDefaults; +import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigObject; +import org.tron.core.config.BeanDefaults; import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -190,12 +191,14 @@ public static class PropertyConfig { public static StorageConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new StorageConfig()); + Config userSection = config.hasPath("storage") + ? BeanDefaults.stripNullLeaves(config.getConfig("storage")) + : ConfigFactory.empty(); // User's storage section takes priority; defaults fill in any omitted scalar keys. // readDbOption() uses hasPath() on the merged section, so user-set optional keys - // (default, defaultM, defaultL) are still detected correctly. - Config section = config.hasPath("storage") - ? BeanDefaults.stripNullLeaves(config.getConfig("storage")).withFallback(defaults) - : defaults; + // (default, defaultM, defaultL) are still detected correctly because they are + // absent from BeanDefaults and only present when the user explicitly set them. + Config section = userSection.withFallback(defaults); StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); sc.rawStorageConfig = section; diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index c0136d06594..2b4c06f194b 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; import org.tron.core.config.BeanDefaults; import lombok.Getter; import lombok.Setter; @@ -31,10 +32,11 @@ public class VmConfig { public static VmConfig fromConfig(Config config) { Config defaults = BeanDefaults.toConfig(new VmConfig()); - Config vmSection = config.hasPath("vm") - ? BeanDefaults.stripNullLeaves(config.getConfig("vm")).withFallback(defaults) - : defaults; - VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); + Config userSection = config.hasPath("vm") + ? BeanDefaults.stripNullLeaves(config.getConfig("vm")) + : ConfigFactory.empty(); + Config section = userSection.withFallback(defaults); + VmConfig vmConfig = ConfigBeanFactory.create(section, VmConfig.class); vmConfig.postProcess(); return vmConfig; } diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 39500a12197..56a919a0f2f 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -307,13 +307,13 @@ public void testShieldedApiLegacyKeyRespected() { } @Test - public void testShieldedApiLegacyKeyTakesPriorityOverModern() { - // Consistent with maxActiveNodesWithSameIp: legacy key presence wins over modern. + public void testShieldedApiModernKeyTakesPriorityOverLegacy() { + // When both keys are present, allowShieldedTransactionApi (modern) wins. NodeConfig nc = NodeConfig.fromConfig( withRef("node {\n" + " allowShieldedTransactionApi = false\n" + " fullNodeAllowShieldedTransaction = true\n" + "}")); - assertTrue(nc.isAllowShieldedTransactionApi()); + assertFalse(nc.isAllowShieldedTransactionApi()); } } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index f3e720f4de4..6da915b0720 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -108,8 +108,9 @@ storage { } node.discovery = { - enable = true - persist = true + enable = true # default: false + persist = true # default: false + # external.ip = "" # default: "" (auto-detect) } # custom stop condition From 7dd0137f3d126ed6e73677510ba3d2dcd5c24177 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 14:07:11 +0800 Subject: [PATCH 22/31] optimize BeanDefaults --- .../org/tron/core/config/BeanDefaults.java | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/BeanDefaults.java b/common/src/main/java/org/tron/core/config/BeanDefaults.java index 1e54bac5724..1b9d8836759 100644 --- a/common/src/main/java/org/tron/core/config/BeanDefaults.java +++ b/common/src/main/java/org/tron/core/config/BeanDefaults.java @@ -87,30 +87,40 @@ private static ConfigObject stripNullObject(ConfigObject obj) { private static Map toMap(Object bean) { Map map = new LinkedHashMap<>(); + BeanInfo info; try { - BeanInfo info = Introspector.getBeanInfo(bean.getClass()); - for (PropertyDescriptor pd : info.getPropertyDescriptors()) { - Method getter = pd.getReadMethod(); - Method setter = pd.getWriteMethod(); - // Skip read-only properties (no setter) — matches ConfigBeanFactory's contract - if (getter == null || setter == null) { - continue; - } - // Use the property name exactly as Introspector produced it. - // ConfigBeanFactory does configProps.get(beanProp.getName()) — the lookup key - // is the property name verbatim, not decapitalized. For ordinary camelCase - // setters (setMaxConnections → "MaxConnections" → decapitalize → "maxConnections") - // Introspector already returns the lowercase form. For setters that start with - // two consecutive uppercase letters (setPBFTEnable → "PBFTEnable") the JavaBean - // spec forbids decapitalization, so pd.getName() == "PBFTEnable" — matching the - // capital-P key that config.conf uses for those fields. + info = Introspector.getBeanInfo(bean.getClass()); + } catch (java.beans.IntrospectionException e) { + // Programming error: bean class does not conform to JavaBean spec. + // Propagate immediately so the misconfigured class is identified at startup, + // rather than returning a silent empty map that produces a confusing + // ConfigException.Missing pointing at the user config. + throw new IllegalStateException("Cannot introspect bean: " + bean.getClass().getName(), e); + } + for (PropertyDescriptor pd : info.getPropertyDescriptors()) { + Method getter = pd.getReadMethod(); + Method setter = pd.getWriteMethod(); + // Skip read-only properties (no setter) — matches ConfigBeanFactory's contract + if (getter == null || setter == null) { + continue; + } + // Use the property name exactly as Introspector produced it. + // ConfigBeanFactory does configProps.get(beanProp.getName()) — the lookup key + // is the property name verbatim, not decapitalized. For ordinary camelCase + // setters (setMaxConnections → "MaxConnections" → decapitalize → "maxConnections") + // Introspector already returns the lowercase form. For setters that start with + // two consecutive uppercase letters (setPBFTEnable → "PBFTEnable") the JavaBean + // spec forbids decapitalization, so pd.getName() == "PBFTEnable" — matching the + // capital-P key that config.conf uses for those fields. + try { String key = pd.getName(); Object value = getter.invoke(bean); map.put(key, toValue(value)); + } catch (Exception ignored) { + // Best-effort: skip individual unresolvable property so that the rest of + // the defaults are still emitted. getter.invoke() is the only realistic + // throw site (InvocationTargetException / IllegalAccessException). } - } catch (Exception ignored) { - // Best-effort: any unresolvable field is simply omitted. - // ConfigBeanFactory will throw with a clear path if a required key is missing. } return map; } From 4bdc025ac70cd231b91cb27362c22d3ad3fdf2e0 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 15:41:57 +0800 Subject: [PATCH 23/31] reject empty batch; use StandardCharsets.UTF_8 --- .../filter/BufferedResponseWrapper.java | 5 +- .../filter/CachedBodyRequestWrapper.java | 9 +++- .../core/services/jsonrpc/JsonRpcServlet.java | 5 ++ .../org/tron/core/jsonrpc/JsonRpcTest.java | 54 +++++++++++++++++++ .../services/jsonrpc/JsonRpcServletTest.java | 11 ++++ 5 files changed, 82 insertions(+), 2 deletions(-) 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 a4cf777d85a..e99dcffde67 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,7 +2,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; @@ -66,7 +68,8 @@ public void setWriteListener(WriteListener writeListener) { } }; - private final PrintWriter writer = new PrintWriter(outputStream, true); + private final PrintWriter writer = + new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true); /** * @param response the wrapped response 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 1b0141b2e5b..ef33f92d25a 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 @@ -4,7 +4,9 @@ import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; @@ -66,7 +68,12 @@ public BufferedReader getReader() { } accessor = BodyAccessor.READER; String encoding = getCharacterEncoding(); - Charset charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8; + Charset charset; + try { + charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8; + } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) { + charset = StandardCharsets.UTF_8; + } return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset)); } } 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 fdbb9685f57..2d2c6f2fed1 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 @@ -33,6 +33,7 @@ public class JsonRpcServlet extends RateLimiterServlet { private enum JsonRpcError { PARSE_ERROR(-32700), + INVALID_REQUEST(-32600), INTERNAL_ERROR(-32603), EXCEED_LIMIT(-32005), RESPONSE_TOO_LARGE(-32003); @@ -104,6 +105,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } boolean isBatch = rootNode.isArray(); + if (isBatch && rootNode.isEmpty()) { + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); + return; + } int batchSize = parameter.getJsonRpcMaxBatchSize(); if (isBatch && batchSize > 0 && rootNode.size() > batchSize) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index bd357101da3..598af6f8d3a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -8,8 +8,10 @@ import java.util.ArrayList; import java.util.BitSet; +import java.util.Collections; import java.util.List; import org.bouncycastle.util.encoders.Hex; +import org.tron.common.parameter.CommonParameter; import org.junit.Assert; import org.junit.Test; import org.tron.common.bloom.Bloom; @@ -242,6 +244,58 @@ public void testLogFilter() { } } + @Test + public void testLogFilterAddressSizeLimit() { + // Two valid 20-byte addresses (40 hex chars with 0x prefix) + String addr1 = "0xaa6612f03443517ced2bdcf27958c22353ceeab9"; + String addr2 = "0xbb7723a04554628ced3cdf38069b433464ffbc0a"; + String addr3 = "0xcc8834b15665739def4de049f17a544575aabcd1"; + + int savedLimit = CommonParameter.getInstance().jsonRpcMaxAddressSize; + try { + CommonParameter.getInstance().jsonRpcMaxAddressSize = 2; + + // Exactly at limit — must not throw + ArrayList atLimit = new ArrayList<>(); + atLimit.add(addr1); + atLimit.add(addr2); + FilterRequest frAtLimit = new FilterRequest(); + frAtLimit.setAddress(atLimit); + try { + new LogFilter(frAtLimit); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail("address list at limit should not throw: " + e.getMessage()); + } + + // One over limit — must throw with expected message + ArrayList overLimit = new ArrayList<>(); + overLimit.add(addr1); + overLimit.add(addr2); + overLimit.add(addr3); + FilterRequest frOverLimit = new FilterRequest(); + frOverLimit.setAddress(overLimit); + try { + new LogFilter(frOverLimit); + Assert.fail("address list over limit should have thrown JsonRpcInvalidParamsException"); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("exceed max addresses:")); + } + + // Limit = 0 means disabled — large list must pass + CommonParameter.getInstance().jsonRpcMaxAddressSize = 0; + ArrayList largeList = new ArrayList<>(Collections.nCopies(500, addr1)); + FilterRequest frDisabled = new FilterRequest(); + frDisabled.setAddress(largeList); + try { + new LogFilter(frDisabled); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail("limit=0 should disable the check: " + e.getMessage()); + } + } finally { + CommonParameter.getInstance().jsonRpcMaxAddressSize = savedLimit; + } + } + private int[] getBloomIndex(String s) { Bloom bloom = Bloom.create(Hash.sha3(ByteArray.fromHexString(s))); BitSet bs = BitSet.valueOf(bloom.getData()); 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 6a6c679070f..4c60b520752 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 @@ -99,6 +99,17 @@ public void batchWithinLimit_proceedsToRpcServer() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + @Test + public void emptyBatch_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse("empty-batch error response must be a single object, not an array", body.isArray()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertTrue(body.get("id").isNull()); + } + @Test public void batchLimitDisabled_largeBatchAllowed() throws Exception { CommonParameter.getInstance().jsonRpcMaxBatchSize = 0; From fd7fdf9cc68a9a184ccc09dc9a9b1b882bcf1ac3 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 15:50:00 +0800 Subject: [PATCH 24/31] reorganize the package import --- framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 598af6f8d3a..5f577194dff 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -11,11 +11,11 @@ import java.util.Collections; import java.util.List; import org.bouncycastle.util.encoders.Hex; -import org.tron.common.parameter.CommonParameter; import org.junit.Assert; import org.junit.Test; import org.tron.common.bloom.Bloom; import org.tron.common.crypto.Hash; +import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; From 2fdb6b8af848d965acc2c7718fea709e749d2c15 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 22:20:49 +0800 Subject: [PATCH 25/31] add some document for CachedBodyRequestWrapper --- .../filter/CachedBodyRequestWrapper.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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 ef33f92d25a..ce4278ddecb 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 @@ -13,11 +13,27 @@ import javax.servlet.http.HttpServletRequestWrapper; /** - * Wraps a request and replays a pre-read body from a byte array. + * Wraps a request to replay a pre-read body from a byte array, + * allowing the body to be read more than once. + * + *

Scope: designed for synchronous, raw-body POST endpoints + * (e.g. JSON-RPC). It is NOT compatible with: + *

    + *
  • {@code application/x-www-form-urlencoded} — cached body cannot back + * {@code getParameter*}.
  • + *
  • multipart — {@code getPart()/getParts()} read from the original + * (already-consumed) stream.
  • + *
  • async non-blocking I/O — see {@code setReadListener}.
  • + *
  • request dispatch / forward chains.
  • + *
+ * + *

Multiple calls to {@code getInputStream()} (or {@code getReader()}) + * are allowed and each returns a fresh stream over the same cached body — + * a deliberate extension of the standard servlet contract. */ public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { - private enum BodyAccessor { NONE, STREAM, READER } + private enum BodyAccessor {NONE, STREAM, READER} private final byte[] body; private BodyAccessor accessor = BodyAccessor.NONE; @@ -57,6 +73,8 @@ public boolean isReady() { @Override public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException( + "async I/O is not supported on cached body"); } }; } From c329b470d6ef890f90f5233c93e12a6fbaea7c2b Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 22:26:11 +0800 Subject: [PATCH 26/31] format the code --- .../org/tron/core/services/filter/CachedBodyRequestWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ce4278ddecb..683fe849f71 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 @@ -33,7 +33,7 @@ */ public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { - private enum BodyAccessor {NONE, STREAM, READER} + private enum BodyAccessor { NONE, STREAM, READER } private final byte[] body; private BodyAccessor accessor = BodyAccessor.NONE; From 998f52f1d906e1baf5e9eecce23d0a27852d902c Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Fri, 8 May 2026 23:08:49 +0800 Subject: [PATCH 27/31] don't continue rest jsonrpc request of batch if overflow --- .../core/services/jsonrpc/JsonRpcServlet.java | 78 +++++++++++++++++-- .../services/jsonrpc/JsonRpcServletTest.java | 65 +++++++++++----- 2 files changed, 119 insertions(+), 24 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 2d2c6f2fed1..c2f202f1fe5 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 @@ -9,6 +9,7 @@ import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; import com.googlecode.jsonrpc4j.ProxyUtil; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -116,29 +117,94 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I return; } + int maxResponseSize = parameter.getJsonRpcMaxResponseSize(); + if (isBatch) { + handleBatch(resp, rootNode, maxResponseSize); + } else { + handleSingle(req, resp, rootNode, body, maxResponseSize); + } + } + + private void handleSingle(HttpServletRequest req, HttpServletResponse resp, + JsonNode rootNode, byte[] body, int maxResponseSize) throws IOException { CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body); BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper( - resp, parameter.getJsonRpcMaxResponseSize()); + resp, maxResponseSize); try { rpcServer.handle(cachedReq, bufferedResp); } catch (RuntimeException e) { logger.error("RPC execution failed", e); - JsonNode idNode = isBatch ? null : rootNode.get("id"); - writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", idNode, isBatch); + writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", + rootNode.get("id"), false); return; } if (bufferedResp.isOverflow()) { - JsonNode idNode = isBatch ? null : rootNode.get("id"); writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, - "Response exceeds the limit of " + parameter.getJsonRpcMaxResponseSize() + " bytes", - idNode, isBatch); + "Response exceeds the limit of " + maxResponseSize + " bytes", + rootNode.get("id"), false); return; } bufferedResp.commitToResponse(); } + private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxResponseSize) + throws IOException { + + ArrayNode batchResult = MAPPER.createArrayNode(); + int accumulatedSize = 2; // "[]" + + for (int i = 0; i < rootNode.size(); i++) { + byte[] subBody; + try { + subBody = MAPPER.writeValueAsBytes(rootNode.get(i)); + } catch (JsonProcessingException e) { + writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true); + return; + } + + ByteArrayOutputStream subOutput = new ByteArrayOutputStream(); + try { + rpcServer.handleRequest(new ByteArrayInputStream(subBody), subOutput); + } catch (RuntimeException e) { + logger.error("RPC execution failed for batch sub-request {}", i, e); + writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true); + return; + } + + byte[] responseBytes = subOutput.toByteArray(); + if (responseBytes.length == 0) { + continue; // notification — no response + } + + // comma separator between array elements + int addition = responseBytes.length + (!batchResult.isEmpty() ? 1 : 0); + if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { + writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, + "Response exceeds the limit of " + maxResponseSize + " bytes", null, true); + return; + } + accumulatedSize += addition; + + JsonNode responseNode; + try { + responseNode = MAPPER.readTree(responseBytes); + } catch (IOException e) { + writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true); + return; + } + batchResult.add(responseNode); + } + + byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); + resp.setContentType("application/json; charset=utf-8"); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentLength(finalBytes.length); + resp.getOutputStream().write(finalBytes); + resp.getOutputStream().flush(); + } + private byte[] readBody(InputStream in) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] tmp = new byte[4096]; 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 4c60b520752..56cc879f045 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 @@ -13,6 +13,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.jsonrpc4j.JsonRpcServer; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import javax.servlet.http.HttpServletRequest; @@ -87,16 +89,20 @@ public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception { @Test public void batchWithinLimit_proceedsToRpcServer() throws Exception { CommonParameter.getInstance().jsonRpcMaxBatchSize = 5; - byte[] rpcResp = "[{\"result\":\"ok\"}]".getBytes(StandardCharsets.UTF_8); + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); doAnswer(inv -> { - HttpServletResponse r = inv.getArgument(1); - r.getOutputStream().write(rpcResp); - return null; - }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2}]"); assertEquals(200, resp.getStatus()); - assertArrayEquals(rpcResp, resp.getContentAsByteArray()); + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertTrue("batch response must be a JSON array", body.isArray()); + assertEquals("each sub-request must produce a response", 2, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); } @Test @@ -113,12 +119,9 @@ public void emptyBatch_returnsInvalidRequest() throws Exception { @Test public void batchLimitDisabled_largeBatchAllowed() throws Exception { CommonParameter.getInstance().jsonRpcMaxBatchSize = 0; - byte[] rpcResp = "[]".getBytes(StandardCharsets.UTF_8); - doAnswer(inv -> { - HttpServletResponse r = inv.getArgument(1); - r.getOutputStream().write(rpcResp); - return null; - }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + // write nothing — simulates notifications (no response expected) + doAnswer(inv -> 0).when(mockRpcServer) + .handleRequest(any(InputStream.class), any(OutputStream.class)); StringBuilder sb = new StringBuilder("["); for (int i = 0; i < 500; i++) { @@ -130,7 +133,9 @@ public void batchLimitDisabled_largeBatchAllowed() throws Exception { sb.append("]"); MockHttpServletResponse resp = doPost(sb.toString()); assertEquals(200, resp.getStatus()); - assertArrayEquals(rpcResp, resp.getContentAsByteArray()); + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals("all notifications produce no response entries", 0, body.size()); } // --- rpcServer.handle exceptions --- @@ -149,7 +154,7 @@ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Except @Test public void batchRpcServerThrows_internalErrorIsArray() throws Exception { doThrow(new RuntimeException("boom")).when(mockRpcServer) - .handle((HttpServletRequest) any(), any()); + .handleRequest(any(InputStream.class), any(OutputStream.class)); MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); assertEquals(200, resp.getStatus()); JsonNode body = MAPPER.readTree(resp.getContentAsString()); @@ -181,10 +186,10 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception { int limit = 50; CommonParameter.getInstance().jsonRpcMaxResponseSize = limit; doAnswer(inv -> { - HttpServletResponse r = inv.getArgument(1); - r.getOutputStream().write(new byte[limit + 1]); - return null; - }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + OutputStream out = inv.getArgument(1); + out.write(new byte[limit + 1]); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]"); assertEquals(200, resp.getStatus()); @@ -193,6 +198,30 @@ public void batchResponseTooLarge_returnsErrorArray() throws Exception { assertEquals(-32003, body.get(0).get("error").get("code").asInt()); } + @Test + public void batchShortCircuitsOnOverflow() throws Exception { + int limit = 50; + CommonParameter.getInstance().jsonRpcMaxResponseSize = limit; + int[] callCount = {0}; + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + callCount[0]++; + if (callCount[0] == 1) { + out.write("{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8)); + } else { + out.write(new byte[limit]); // triggers overflow when added to accumulated size + } + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("overflow response must be an array", body.isArray()); + assertEquals(-32003, body.get(0).get("error").get("code").asInt()); + assertEquals("third sub-request must not be executed after overflow", 2, callCount[0]); + } + // --- normal path --- @Test From 498c9da7586ed4b07ceb9978d8f5a7aa60480837 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Sat, 9 May 2026 17:27:05 +0800 Subject: [PATCH 28/31] fix the bug of only one erro is return is the n-th request is overflow --- .../core/services/jsonrpc/JsonRpcServlet.java | 41 +++++++++++++++---- .../services/jsonrpc/JsonRpcServletTest.java | 15 +++++-- 2 files changed, 45 insertions(+), 11 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 c2f202f1fe5..1505bdf9118 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 @@ -154,11 +154,24 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes ArrayNode batchResult = MAPPER.createArrayNode(); int accumulatedSize = 2; // "[]" + boolean overflow = false; for (int i = 0; i < rootNode.size(); i++) { + JsonNode subRequest = rootNode.get(i); + + if (overflow) { + // Notifications (no "id") do not get a response even on overflow. + if (subRequest.has("id")) { + batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE, + "Response exceeds the limit of " + maxResponseSize + " bytes", + subRequest.get("id"))); + } + continue; + } + byte[] subBody; try { - subBody = MAPPER.writeValueAsBytes(rootNode.get(i)); + subBody = MAPPER.writeValueAsBytes(subRequest); } catch (JsonProcessingException e) { writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true); return; @@ -178,12 +191,14 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes continue; // notification — no response } - // comma separator between array elements + // comma(,) separator between array elements int addition = responseBytes.length + (!batchResult.isEmpty() ? 1 : 0); if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { - writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, - "Response exceeds the limit of " + maxResponseSize + " bytes", null, true); - return; + overflow = true; + batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE, + "Response exceeds the limit of " + maxResponseSize + " bytes", + subRequest.get("id"))); + continue; } accumulatedSize += addition; @@ -197,6 +212,13 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes batchResult.add(responseNode); } + // JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects. + if (batchResult.isEmpty()) { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentLength(0); + return; + } + byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); resp.setContentType("application/json; charset=utf-8"); resp.setStatus(HttpServletResponse.SC_OK); @@ -215,8 +237,7 @@ private byte[] readBody(InputStream in) throws IOException { return buffer.toByteArray(); } - private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message, - JsonNode id, boolean isBatch) throws IOException { + private ObjectNode buildErrorNode(JsonRpcError error, String message, JsonNode id) { ObjectNode errorObj = MAPPER.createObjectNode(); errorObj.put("jsonrpc", "2.0"); ObjectNode errNode = errorObj.putObject("error"); @@ -227,6 +248,12 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { errorObj.putNull("id"); } + return errorObj; + } + + private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message, + JsonNode id, boolean isBatch) throws IOException { + ObjectNode errorObj = buildErrorNode(error, message, id); byte[] bytes; if (isBatch) { ArrayNode arr = MAPPER.createArrayNode(); 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 56cc879f045..fa45ca48876 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 @@ -133,9 +133,9 @@ public void batchLimitDisabled_largeBatchAllowed() throws Exception { sb.append("]"); MockHttpServletResponse resp = doPost(sb.toString()); assertEquals(200, resp.getStatus()); - JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); - assertTrue("response must be a JSON array", body.isArray()); - assertEquals("all notifications produce no response entries", 0, body.size()); + assertEquals("all-notification batch must return empty body per JSON-RPC 2.0 §6", + 0, resp.getContentLength()); + assertEquals("", resp.getContentAsString()); } // --- rpcServer.handle exceptions --- @@ -218,7 +218,14 @@ public void batchShortCircuitsOnOverflow() throws Exception { assertEquals(200, resp.getStatus()); JsonNode body = MAPPER.readTree(resp.getContentAsString()); assertTrue("overflow response must be an array", body.isArray()); - assertEquals(-32003, body.get(0).get("error").get("code").asInt()); + // Geth-compatible: previous successes are preserved; overflow item and remaining + // unexecuted items each get a -32003 error with their original id. + assertEquals(3, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); + assertEquals(-32003, body.get(1).get("error").get("code").asInt()); + assertEquals(2, body.get(1).get("id").asInt()); + assertEquals(-32003, body.get(2).get("error").get("code").asInt()); + assertEquals(3, body.get(2).get("id").asInt()); assertEquals("third sub-request must not be executed after overflow", 2, callCount[0]); } From 39087705379f2f88428e6cfa0037b25c953457ee Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Sat, 9 May 2026 21:02:47 +0800 Subject: [PATCH 29/31] use ContentType as application/json-rpc --- .../java/org/tron/core/services/jsonrpc/JsonRpcServlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 1505bdf9118..1759308502b 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 @@ -220,7 +220,7 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes } byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); - resp.setContentType("application/json; charset=utf-8"); + resp.setContentType("application/json-rpc; charset=utf-8"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(finalBytes.length); resp.getOutputStream().write(finalBytes); @@ -262,7 +262,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { bytes = MAPPER.writeValueAsBytes(errorObj); } - resp.setContentType("application/json; charset=utf-8"); + resp.setContentType("application/json-rpc; charset=utf-8"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); From f28beb3d28c4df6edf00c53fde0d4939c839a20b Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Sat, 9 May 2026 22:43:24 +0800 Subject: [PATCH 30/31] remove reduant maxBlockFilterNum; optimize commitToResponse --- common/src/main/resources/reference.conf | 23 +++++++------------ .../filter/BufferedResponseWrapper.java | 7 ++++++ .../core/services/jsonrpc/JsonRpcServlet.java | 3 +-- framework/src/main/resources/config.conf | 4 ++-- .../filter/BufferedResponseWrapperTest.java | 23 +++++++++++++++++++ 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 473b8ecef67..76225aa0bed 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -380,27 +380,20 @@ node { httpPBFTEnable = false httpPBFTPort = 8565 - # Maximum blocks range for eth_getLogs, >0 otherwise no limit + # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit maxBlockRange = 5000 - - # Maximum topics within a topic criteria, >0 otherwise no limit + # Allowed max address count in filter request, default: 1000, <=0 means no limit + maxAddressSize = 1000 + # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit maxSubTopics = 1000 - - # Maximum number for blockFilter. >0 otherwise no limit + # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit maxBlockFilterNum = 50000 - - # Maximum number of requests in a JSON-RPC batch, >0 otherwise no limit + # Allowed batch size, default: 100, <=0 means no limit maxBatchSize = 100 - - # Maximum response body size in bytes for JSON-RPC (default 25MB), >0 otherwise no limit + # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit maxResponseSize = 26214400 - - # Maximum number of addresses in a single JSON-RPC request, >0 otherwise no limit - maxAddressSize = 1000 - - # Maximum number of concurrent eth_newFilter registrations, >0 otherwise no limit + # Allowed maximum number for newFilter, <=0 means no limit maxLogFilterNum = 20000 - # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize. maxMessageSize = 4M } 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 e99dcffde67..7076746b2a0 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 @@ -160,6 +160,13 @@ public void commitToResponse() throws IOException { throw new IllegalStateException("commitToResponse() already called"); } committed = true; + // Flush the PrintWriter's OutputStreamWriter encoder into our ByteArrayOutputStream. + // PrintWriter(autoFlush=true) only auto-flushes on println/printf/format, not print/write, + // so bytes can remain buffered in the encoder until an explicit flush. + writer.flush(); + if (overflow) { + return; + } if (contentType != null) { actual.setContentType(contentType); } 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 1759308502b..2093930ca98 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 @@ -140,13 +140,12 @@ private void handleSingle(HttpServletRequest req, HttpServletResponse resp, return; } + bufferedResp.commitToResponse(); if (bufferedResp.isOverflow()) { writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE, "Response exceeds the limit of " + maxResponseSize + " bytes", rootNode.get("id"), false); - return; } - bufferedResp.commitToResponse(); } private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxResponseSize) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 15277fa9f75..b180ecd6d10 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -400,10 +400,10 @@ node { maxBatchSize = 100 # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit maxResponseSize = 26214400 - # Allowed maximum number for blockFilter, <=0 means no limit - maxBlockFilterNum = 50000 # Allowed maximum number for newFilter, <=0 means no limit maxLogFilterNum = 20000 + # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize. + maxMessageSize = 4M } # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, 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 index 76de2d2db2e..d7828fa5cd0 100644 --- a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java @@ -261,4 +261,27 @@ public void writeViaWriter_commitToResponse_flushesBody() throws IOException { w.commitToResponse(); assertEquals("hello", mockResp.getContentAsString()); } + + @Test + public void writeViaWriter_noExplicitFlush_commitToResponse_flushesBody() throws IOException { + // Regression: PrintWriter(autoFlush=true) does NOT flush on plain print(); bytes can sit + // in the OutputStreamWriter encoder until commitToResponse() flushes the writer internally. + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0); + w.getWriter().print("hello"); + w.commitToResponse(); + assertEquals("hello", mockResp.getContentAsString()); + assertEquals(5, mockResp.getContentLength()); + } + + @Test + public void writeViaWriter_noExplicitFlush_flushTripsOverflow() throws IOException { + // Regression: bytes buffered in the encoder may push the total past maxBytes when + // commitToResponse() flushes — overflow must be detected and nothing written to actual. + BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 3); + w.getWriter().print("hello"); // 5 bytes, not yet in ByteArrayOutputStream + assertFalse("overflow must not trigger before flush", w.isOverflow()); + w.commitToResponse(); + assertTrue("flush inside commitToResponse must trip overflow", w.isOverflow()); + assertEquals(0, mockResp.getContentAsByteArray().length); + } } From 86f8d74f56fa0b2c7126686060d2d6612506e7b0 Mon Sep 17 00:00:00 2001 From: jiangyuanshu <317787106@qq.com> Date: Sat, 9 May 2026 23:11:16 +0800 Subject: [PATCH 31/31] fix bug of BeanDefaultsTest --- common/src/test/java/org/tron/core/config/BeanDefaultsTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java index 88107484b01..fff457e3e00 100644 --- a/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java +++ b/common/src/test/java/org/tron/core/config/BeanDefaultsTest.java @@ -171,7 +171,6 @@ public void metricsConfig_defaultValues() { public void metricsConfig_roundTrip() { Config defaults = BeanDefaults.toConfig(new MetricsConfig()); MetricsConfig mc = ConfigBeanFactory.create(defaults, MetricsConfig.class); - Assert.assertFalse(mc.isStorageEnable()); Assert.assertEquals(9527, mc.getPrometheus().getPort()); }