From 746abf993cd64548539c6c2c09fbb40ffbcd8269 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 8 Apr 2026 14:50:57 +0800 Subject: [PATCH 01/17] refactor(framework): remove unused Wallet.getMerkleTreeOfBlock and Wallet.deployContract Both methods have zero callers in production code. getMerkleTreeOfBlock was an orphaned shielded transaction helper; deployContract was a no-op stub that simply returned the input transaction unchanged. --- .../src/main/java/org/tron/core/Wallet.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..b13ef560fec 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -2207,23 +2207,6 @@ public IncrementalMerkleVoucherInfo getMerkleTreeVoucherInfo(OutputPointInfo req return result.build(); } - public IncrementalMerkleTree getMerkleTreeOfBlock(long blockNum) throws ZksnarkException { - checkAllowShieldedTransactionApi(); - if (blockNum < 0) { - return null; - } - - try { - if (chainBaseManager.getMerkleTreeIndexStore().has(ByteArray.fromLong(blockNum))) { - return IncrementalMerkleTree - .parseFrom(chainBaseManager.getMerkleTreeIndexStore().get(blockNum)); - } - } catch (Exception ex) { - logger.error("GetMerkleTreeOfBlock failed, blockNum:{}", blockNum, ex); - } - - return null; - } public long getShieldedTransactionFee() { return chainBaseManager.getDynamicPropertiesStore().getShieldedTransactionFee(); @@ -2958,13 +2941,6 @@ public MarketOrderList getMarketOrderListByPair(byte[] sellTokenId, byte[] buyTo return builder.build(); } - public Transaction deployContract(TransactionCapsule trxCap) { - - // do nothing, so can add some useful function later - // trxCap contract para cacheUnpackValue has value - - return trxCap.getInstance(); - } public Transaction triggerContract(TriggerSmartContract triggerSmartContract, From 0fe5d6f03b49327f6f400a17d66d8b959c84a7d2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 8 Apr 2026 15:10:25 +0800 Subject: [PATCH 02/17] fix(api): replace Class.forName with whitelist in RateLimiterServlet Replace reflection-based adapter loading with a static whitelist map to prevent arbitrary class instantiation via tampered config. Only pre-approved rate limiter adapters can now be loaded. --- .../services/http/RateLimiterServlet.java | 68 ++++++++-------- .../http/RateLimiterServletWhitelistTest.java | 78 +++++++++++++++++++ 2 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..d3372e43fe1 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,7 +3,9 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,7 +33,22 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + private static final Map> ALLOWED_ADAPTERS; + private static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + Map> m = new HashMap<>(); + for (Class c : new Class[]{ + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class + }) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @@ -40,42 +57,21 @@ public abstract class RateLimiterServlet extends HttpServlet { private void addRateContainer() { RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } - } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); + + String cName = (item != null) ? item.getStrategy() : DEFAULT_ADAPTER_NAME; + String params = (item != null) ? item.getParams() : QpsStrategy.DEFAULT_QPS_PARAM; + + try { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw new IllegalArgumentException( + "Unknown rate limiter adapter (not in whitelist): " + cName); } + IRateLimiter rateLimiter = c.getConstructor(String.class).newInstance(params); + container.add(KEY_PREFIX_HTTP, name, rateLimiter); + } catch (Exception e) { + this.throwTronError(cName, params, name, e); } } diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java new file mode 100644 index 00000000000..81d8332789f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java @@ -0,0 +1,78 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies that RateLimiterServlet uses a strict whitelist + * instead of Class.forName(), preventing arbitrary class loading + * via a tampered config file. + */ +public class RateLimiterServletWhitelistTest { + + private static final String GLOBAL_PREEMPTIBLE = GlobalPreemptibleAdapter.class.getSimpleName(); + private static final String QPS_RATE_LIMITER = QpsRateLimiterAdapter.class.getSimpleName(); + private static final String IP_QPS_RATE_LIMITER = IPQPSRateLimiterAdapter.class.getSimpleName(); + private static final String DEFAULT_BASE_QPS = DefaultBaseQqsAdapter.class.getSimpleName(); + + private static Map> allowedAdapters; + + @SuppressWarnings("unchecked") + @BeforeClass + public static void loadWhitelist() throws Exception { + Field f = RateLimiterServlet.class.getDeclaredField("ALLOWED_ADAPTERS"); + f.setAccessible(true); + allowedAdapters = (Map>) f.get(null); + } + + @Test + public void testWhitelistContents() { + assertNotNull(allowedAdapters.get(GLOBAL_PREEMPTIBLE)); + assertNotNull(allowedAdapters.get(QPS_RATE_LIMITER)); + assertNotNull(allowedAdapters.get(IP_QPS_RATE_LIMITER)); + assertNotNull(allowedAdapters.get(DEFAULT_BASE_QPS)); + + assertTrue(GlobalPreemptibleAdapter.class + .isAssignableFrom(allowedAdapters.get(GLOBAL_PREEMPTIBLE))); + assertTrue(QpsRateLimiterAdapter.class + .isAssignableFrom(allowedAdapters.get(QPS_RATE_LIMITER))); + assertTrue(IPQPSRateLimiterAdapter.class + .isAssignableFrom(allowedAdapters.get(IP_QPS_RATE_LIMITER))); + assertTrue(DefaultBaseQqsAdapter.class + .isAssignableFrom(allowedAdapters.get(DEFAULT_BASE_QPS))); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testWhitelistIsUnmodifiable() { + try { + allowedAdapters.put("EvilAdapter", DefaultBaseQqsAdapter.class); + assertFalse("Whitelist should be unmodifiable", true); + } catch (UnsupportedOperationException e) { + // expected + } + } + + @Test + public void testWhitelistSize() { + assertTrue("Whitelist should contain exactly 4 adapters", + allowedAdapters.size() == 4); + } +} From 09804886ab474e277e45339cf94be45a2c069516 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 6 Mar 2026 17:29:11 +0800 Subject: [PATCH 03/17] perf(api): optimize block JSON serialization in printBlockList and printBlockToJSON 1. remove redundant full BlockList serialization in printBlockList 2. serialize only block_header instead of entire block in printBlockToJSON 3. enhance testPrintBlockList to verify JSON structure after refactoring --- .../java/org/tron/core/services/http/Util.java | 6 ++++-- .../tron/core/services/http/UtilMockTest.java | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..a0e8c77d646 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -95,7 +95,7 @@ public static String printErrorMsg(Exception e) { public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = new JSONObject(); JSONArray jsonArray = new JSONArray(); blocks.stream().forEach(block -> jsonArray.add(printBlockToJSON(block, selfType))); jsonObject.put("block", jsonArray); @@ -110,8 +110,10 @@ public static String printBlock(Block block, boolean selfType) { public static JSONObject printBlockToJSON(Block block, boolean selfType) { BlockCapsule blockCapsule = new BlockCapsule(block); String blockID = ByteArray.toHexString(blockCapsule.getBlockId().getBytes()); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(block, selfType)); + JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); + jsonObject.put("block_header", + JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 221c5a7a165..1e3d011b9ce 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.http; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; @@ -44,7 +45,7 @@ public void testPrintTransactionFee() { public void testPrintBlockList() { BlockCapsule blockCapsule1 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - BlockCapsule blockCapsule2 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + BlockCapsule blockCapsule2 = new BlockCapsule(2, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); GrpcAPI.BlockList list = GrpcAPI.BlockList.newBuilder() .addBlock(blockCapsule1.getInstance()) @@ -52,6 +53,20 @@ public void testPrintBlockList() { .build(); String out = Util.printBlockList(list, true); Assert.assertNotNull(out); + + JSONObject json = JSONObject.parseObject(out); + Assert.assertTrue(json.containsKey("block")); + JSONArray blockArray = json.getJSONArray("block"); + Assert.assertEquals(2, blockArray.size()); + + // verify each block has correct structure + for (int i = 0; i < blockArray.size(); i++) { + JSONObject blockJson = blockArray.getJSONObject(i); + Assert.assertTrue(blockJson.containsKey("blockID")); + Assert.assertTrue(blockJson.containsKey("block_header")); + Assert.assertFalse(blockJson.getString("blockID").isEmpty()); + Assert.assertNotNull(blockJson.getJSONObject("block_header")); + } } @Test From 5080779f5fdc9553d302d67be23bec09cbfcb180 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 8 Apr 2026 19:45:36 +0800 Subject: [PATCH 04/17] fix(api): handle JSONException in checkGetParam and reduce Solidity log level Wrap JSON.parseObject in checkGetParam with try-catch to gracefully handle invalid JSON input instead of propagating the exception. Downgrade Solidity NonUniqueObjectException log from error to debug as it is an expected condition. Add tests for invalid JSON and JSON array inputs. --- .../org/tron/core/services/RpcApiService.java | 2 +- .../org/tron/core/services/http/Util.java | 10 +++- .../tron/core/services/http/UtilMockTest.java | 56 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 63e7ba03fc7..bc50b79a36f 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -418,7 +418,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.error("Solidity NonUniqueObjectException: {}", e.getMessage()); + logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index a0e8c77d646..1742a230cd2 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -543,9 +543,13 @@ private static String checkGetParam(HttpServletRequest request, String key) thro return null; } - JSONObject jsonObject = JSON.parseObject(value); - if (jsonObject != null) { - return jsonObject.getString(key); + try { + JSONObject jsonObject = JSON.parseObject(value); + if (jsonObject != null) { + return jsonObject.getString(key); + } + } catch (com.alibaba.fastjson.JSONException e) { + logger.debug("Invalid JSON input: {}", e.getMessage()); } return null; } diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 1e3d011b9ce..e7c0abcd848 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -4,6 +4,8 @@ import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import java.io.BufferedReader; +import java.io.StringReader; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.List; @@ -13,6 +15,8 @@ import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; + +import javax.servlet.http.HttpServletRequest; import org.tron.api.GrpcAPI; import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; @@ -237,6 +241,58 @@ public void testValidateParameter() { ); } + @Test + public void testGetAddressWithInvalidJsonBody() throws Exception { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getContentType()).thenReturn("application/json"); + + // plain text, not JSON + BufferedReader reader = new BufferedReader(new StringReader("not a json")); + Mockito.when(request.getInputStream()).thenReturn( + new javax.servlet.ServletInputStream() { + private final java.io.InputStream in = + new java.io.ByteArrayInputStream("not a json".getBytes()); + + @Override public int read() throws java.io.IOException { return in.read(); } + + @Override public boolean isFinished() { return false; } + + @Override public boolean isReady() { return true; } + + @Override public void setReadListener(javax.servlet.ReadListener l) { } + }); + + byte[] address = Util.getAddress(request); + Assert.assertNull(address); + } + + @Test + public void testGetAddressWithJsonArrayBody() throws Exception { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getMethod()).thenReturn("POST"); + Mockito.when(request.getContentType()).thenReturn("application/json"); + + // JSON array instead of JSON object + String body = "[\"address1\",\"address2\"]"; + Mockito.when(request.getInputStream()).thenReturn( + new javax.servlet.ServletInputStream() { + private final java.io.InputStream in = + new java.io.ByteArrayInputStream(body.getBytes()); + + @Override public int read() throws java.io.IOException { return in.read(); } + + @Override public boolean isFinished() { return false; } + + @Override public boolean isReady() { return true; } + + @Override public void setReadListener(javax.servlet.ReadListener l) { } + }); + + byte[] address = Util.getAddress(request); + Assert.assertNull(address); + } + @Test public void testGetJsonString() { String str = ""; From 508cf88a9efc0ab83e1de20aabad9c8d4e1d8b4d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 9 Apr 2026 17:27:16 +0800 Subject: [PATCH 05/17] feat(api): add length guard for hexToBigInteger to prevent CPU DoS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BigInteger(String, 16) has O(n²) complexity on large inputs. A multi-MB hex string within the 4MB HTTP body limit can consume seconds of CPU. Add a 66-char limit (0x + 64 hex = uint256 max) to hexToBigInteger, covering all 6 JSON-RPC call sites. Also add null checks to jsonHexToLong and jsonHexToInt (no length guards needed — parseLong and parseInt self-protect via per-character overflow detection). --- .../java/org/tron/common/utils/ByteArray.java | 13 +++++++-- .../org/tron/common/utils/ByteArrayTest.java | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/org/tron/common/utils/ByteArray.java b/common/src/main/java/org/tron/common/utils/ByteArray.java index d0ac4cbaddf..a767f9c8b14 100644 --- a/common/src/main/java/org/tron/common/utils/ByteArray.java +++ b/common/src/main/java/org/tron/common/utils/ByteArray.java @@ -143,7 +143,16 @@ public static String toJsonHex(String x) { return "0x" + x; } + /** + * Max length for hex input to hexToBigInteger: 0x + 64 hex digits (uint256). + */ + private static final int MAX_HEX_BIG_INTEGER_LEN = 66; + public static BigInteger hexToBigInteger(String input) { + if (input == null || input.length() > MAX_HEX_BIG_INTEGER_LEN) { + throw new IllegalArgumentException( + "hex input is null or exceeds max length: " + MAX_HEX_BIG_INTEGER_LEN); + } if (input.startsWith("0x")) { return new BigInteger(input.substring(2), 16); } else { @@ -152,7 +161,7 @@ public static BigInteger hexToBigInteger(String input) { } public static long jsonHexToLong(String x) throws JsonRpcInvalidParamsException { - if (!x.startsWith("0x")) { + if (x == null || !x.startsWith("0x")) { throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); } x = x.substring(2); @@ -160,7 +169,7 @@ public static long jsonHexToLong(String x) throws JsonRpcInvalidParamsException } public static int jsonHexToInt(String x) throws Exception { - if (!x.startsWith("0x")) { + if (x == null || !x.startsWith("0x")) { throw new Exception("Incorrect hex syntax"); } x = x.substring(2); diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index c0db8c4b418..234622c85fe 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -106,6 +106,33 @@ public void testJsonHexToInt_ValidHex() { assertThrows(Exception.class, () -> ByteArray.jsonHexToInt("1A")); } + @Test + public void testHexToBigIntegerRejectsOversizedInput() { + // 0x + 64 hex = 66 chars, valid uint256 + String maxValid = "0x" + new String(new char[64]).replace('\0', 'f'); + assertEquals(66, maxValid.length()); + ByteArray.hexToBigInteger(maxValid); // should not throw + + // 0x + 65 hex = 67 chars, exceeds limit + String tooLong = "0x" + new String(new char[65]).replace('\0', 'a'); + try { + ByteArray.hexToBigInteger(tooLong); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("exceeds max length")); + } + } + + @Test + public void testHexToBigIntegerRejectsNull() { + try { + ByteArray.hexToBigInteger(null); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("null")); + } + } + @Test public void testFromHexWithPrefix() { String input = "0x1A3F"; From 29be3dba1088b781a26cbb539aa22cf30896f43f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 16 Apr 2026 17:54:33 +0800 Subject: [PATCH 06/17] fix(api): relax hexToBigInteger length limit, improve tests and code quality - Relax MAX_HEX_BIG_INTEGER_LEN from 66 to 100 to cover both hex and decimal uint256 representations - Split null/length error messages in hexToBigInteger for clarity - Change ALLOWED_ADAPTERS to package-private, remove reflection from RateLimiterServletWhitelistTest - Remove unused BufferedReader in UtilMockTest - Add test for empty string input to hexToBigInteger - Add test for printBlockToJSON with empty transactions --- .../java/org/tron/common/utils/ByteArray.java | 11 +++--- .../services/http/RateLimiterServlet.java | 2 +- .../org/tron/common/utils/ByteArrayTest.java | 15 +++++--- .../http/RateLimiterServletWhitelistTest.java | 34 ++++++------------- .../tron/core/services/http/UtilMockTest.java | 15 ++++++-- 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/common/src/main/java/org/tron/common/utils/ByteArray.java b/common/src/main/java/org/tron/common/utils/ByteArray.java index a767f9c8b14..044fd066bf1 100644 --- a/common/src/main/java/org/tron/common/utils/ByteArray.java +++ b/common/src/main/java/org/tron/common/utils/ByteArray.java @@ -144,14 +144,17 @@ public static String toJsonHex(String x) { } /** - * Max length for hex input to hexToBigInteger: 0x + 64 hex digits (uint256). + * Max allowed length for input to hexToBigInteger. */ - private static final int MAX_HEX_BIG_INTEGER_LEN = 66; + private static final int MAX_HEX_BIG_INTEGER_LEN = 100; public static BigInteger hexToBigInteger(String input) { - if (input == null || input.length() > MAX_HEX_BIG_INTEGER_LEN) { + if (input == null) { + throw new IllegalArgumentException("hex input is null"); + } + if (input.length() > MAX_HEX_BIG_INTEGER_LEN) { throw new IllegalArgumentException( - "hex input is null or exceeds max length: " + MAX_HEX_BIG_INTEGER_LEN); + "hex input exceeds max length: " + MAX_HEX_BIG_INTEGER_LEN); } if (input.startsWith("0x")) { return new BigInteger(input.substring(2), 16); diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index d3372e43fe1..083c663da90 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -34,7 +34,7 @@ public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final Map> ALLOWED_ADAPTERS; + static final Map> ALLOWED_ADAPTERS; private static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); static { diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index 234622c85fe..cae51b981ce 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -108,13 +108,13 @@ public void testJsonHexToInt_ValidHex() { @Test public void testHexToBigIntegerRejectsOversizedInput() { - // 0x + 64 hex = 66 chars, valid uint256 - String maxValid = "0x" + new String(new char[64]).replace('\0', 'f'); - assertEquals(66, maxValid.length()); + // 0x + 98 hex = 100 chars, at the limit + String maxValid = "0x" + new String(new char[98]).replace('\0', 'f'); + assertEquals(100, maxValid.length()); ByteArray.hexToBigInteger(maxValid); // should not throw - // 0x + 65 hex = 67 chars, exceeds limit - String tooLong = "0x" + new String(new char[65]).replace('\0', 'a'); + // 0x + 99 hex = 101 chars, exceeds limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); try { ByteArray.hexToBigInteger(tooLong); fail("expected IllegalArgumentException"); @@ -123,6 +123,11 @@ public void testHexToBigIntegerRejectsOversizedInput() { } } + @Test(expected = NumberFormatException.class) + public void testHexToBigIntegerRejectsEmptyString() { + ByteArray.hexToBigInteger(""); + } + @Test public void testHexToBigIntegerRejectsNull() { try { diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java index 81d8332789f..86387f81bcb 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java @@ -5,9 +5,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import java.lang.reflect.Field; import java.util.Map; -import org.junit.BeforeClass; import org.junit.Test; import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; @@ -22,36 +20,24 @@ */ public class RateLimiterServletWhitelistTest { - private static final String GLOBAL_PREEMPTIBLE = GlobalPreemptibleAdapter.class.getSimpleName(); - private static final String QPS_RATE_LIMITER = QpsRateLimiterAdapter.class.getSimpleName(); - private static final String IP_QPS_RATE_LIMITER = IPQPSRateLimiterAdapter.class.getSimpleName(); - private static final String DEFAULT_BASE_QPS = DefaultBaseQqsAdapter.class.getSimpleName(); - - private static Map> allowedAdapters; - - @SuppressWarnings("unchecked") - @BeforeClass - public static void loadWhitelist() throws Exception { - Field f = RateLimiterServlet.class.getDeclaredField("ALLOWED_ADAPTERS"); - f.setAccessible(true); - allowedAdapters = (Map>) f.get(null); - } + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; @Test public void testWhitelistContents() { - assertNotNull(allowedAdapters.get(GLOBAL_PREEMPTIBLE)); - assertNotNull(allowedAdapters.get(QPS_RATE_LIMITER)); - assertNotNull(allowedAdapters.get(IP_QPS_RATE_LIMITER)); - assertNotNull(allowedAdapters.get(DEFAULT_BASE_QPS)); + assertNotNull(allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertNotNull(allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertNotNull(allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertNotNull(allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); assertTrue(GlobalPreemptibleAdapter.class - .isAssignableFrom(allowedAdapters.get(GLOBAL_PREEMPTIBLE))); + .isAssignableFrom(allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName()))); assertTrue(QpsRateLimiterAdapter.class - .isAssignableFrom(allowedAdapters.get(QPS_RATE_LIMITER))); + .isAssignableFrom(allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName()))); assertTrue(IPQPSRateLimiterAdapter.class - .isAssignableFrom(allowedAdapters.get(IP_QPS_RATE_LIMITER))); + .isAssignableFrom(allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName()))); assertTrue(DefaultBaseQqsAdapter.class - .isAssignableFrom(allowedAdapters.get(DEFAULT_BASE_QPS))); + .isAssignableFrom(allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName()))); } @Test diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index e7c0abcd848..847eac0e2f3 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -4,8 +4,6 @@ import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; -import java.io.BufferedReader; -import java.io.StringReader; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.List; @@ -73,6 +71,18 @@ public void testPrintBlockList() { } } + @Test + public void testPrintBlockToJSONEmptyTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertFalse(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + Assert.assertNotNull(json.getJSONObject("block_header")); + } + @Test public void testPrintTransactionList() { TransactionCapsule transactionCapsule = getTransactionCapsuleExample(); @@ -248,7 +258,6 @@ public void testGetAddressWithInvalidJsonBody() throws Exception { Mockito.when(request.getContentType()).thenReturn("application/json"); // plain text, not JSON - BufferedReader reader = new BufferedReader(new StringReader("not a json")); Mockito.when(request.getInputStream()).thenReturn( new javax.servlet.ServletInputStream() { private final java.io.InputStream in = From 7e966c9529303781bab2fe9bae75ef152040f693 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 16 Apr 2026 18:05:33 +0800 Subject: [PATCH 07/17] fix(api): address PR review feedback on style and defensive defaults - Add comment explaining intentional MAX_HEX_BIG_INTEGER_LEN = 100 - Fall back to default adapter when strategy/params is null or empty in RateLimiterServlet to avoid startup regression - Fix import order in UtilMockTest (javax before org) - Expand one-line ServletInputStream overrides to satisfy checkstyle --- .../java/org/tron/common/utils/ByteArray.java | 3 ++ .../services/http/RateLimiterServlet.java | 8 +++- .../tron/core/services/http/UtilMockTest.java | 41 ++++++++++++++----- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/org/tron/common/utils/ByteArray.java b/common/src/main/java/org/tron/common/utils/ByteArray.java index 044fd066bf1..1577d9ea6eb 100644 --- a/common/src/main/java/org/tron/common/utils/ByteArray.java +++ b/common/src/main/java/org/tron/common/utils/ByteArray.java @@ -145,6 +145,9 @@ public static String toJsonHex(String x) { /** * Max allowed length for input to hexToBigInteger. + * Covers both hex (0x + 64 = 66 for uint256) and decimal (78 for uint256) + * representations, with headroom. The exact value is not critical for + * performance — BigInteger parsing at this scale is negligible. */ private static final int MAX_HEX_BIG_INTEGER_LEN = 100; diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 083c663da90..75bb59a3624 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -59,8 +59,12 @@ private void addRateContainer() { .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); final String name = getClass().getSimpleName(); - String cName = (item != null) ? item.getStrategy() : DEFAULT_ADAPTER_NAME; - String params = (item != null) ? item.getParams() : QpsStrategy.DEFAULT_QPS_PARAM; + String cName = (item == null || Strings.isNullOrEmpty(item.getStrategy())) + ? DEFAULT_ADAPTER_NAME + : item.getStrategy(); + String params = (item == null || Strings.isNullOrEmpty(item.getParams())) + ? QpsStrategy.DEFAULT_QPS_PARAM + : item.getParams(); try { Class c = ALLOWED_ADAPTERS.get(cName); diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 847eac0e2f3..fdbc395efe7 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -7,14 +7,13 @@ import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.List; +import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; - -import javax.servlet.http.HttpServletRequest; import org.tron.api.GrpcAPI; import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; @@ -263,13 +262,24 @@ public void testGetAddressWithInvalidJsonBody() throws Exception { private final java.io.InputStream in = new java.io.ByteArrayInputStream("not a json".getBytes()); - @Override public int read() throws java.io.IOException { return in.read(); } + @Override + public int read() throws java.io.IOException { + return in.read(); + } - @Override public boolean isFinished() { return false; } + @Override + public boolean isFinished() { + return false; + } - @Override public boolean isReady() { return true; } + @Override + public boolean isReady() { + return true; + } - @Override public void setReadListener(javax.servlet.ReadListener l) { } + @Override + public void setReadListener(javax.servlet.ReadListener l) { + } }); byte[] address = Util.getAddress(request); @@ -289,13 +299,24 @@ public void testGetAddressWithJsonArrayBody() throws Exception { private final java.io.InputStream in = new java.io.ByteArrayInputStream(body.getBytes()); - @Override public int read() throws java.io.IOException { return in.read(); } + @Override + public int read() throws java.io.IOException { + return in.read(); + } - @Override public boolean isFinished() { return false; } + @Override + public boolean isFinished() { + return false; + } - @Override public boolean isReady() { return true; } + @Override + public boolean isReady() { + return true; + } - @Override public void setReadListener(javax.servlet.ReadListener l) { } + @Override + public void setReadListener(javax.servlet.ReadListener l) { + } }); byte[] address = Util.getAddress(request); From 88b7f26867b983bc90a93cae88e555faf7f69708 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 16 Apr 2026 20:30:59 +0800 Subject: [PATCH 08/17] test(api): add printBlockToJSON with-transactions and decimal BigInteger tests Cover two gaps identified in review: - printBlockToJSON with a block containing transactions verifies the transactions array structure (txID, raw_data) - hexToBigInteger decimal path verifies the non-hex branch works correctly for "0", "1", and "12345" --- .../org/tron/common/utils/ByteArrayTest.java | 8 +++++++ .../tron/core/services/http/UtilMockTest.java | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index cae51b981ce..89101738d26 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -24,6 +24,7 @@ import static org.tron.common.utils.ByteArray.fromHex; import static org.tron.common.utils.ByteArray.jsonHexToInt; +import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; @@ -138,6 +139,13 @@ public void testHexToBigIntegerRejectsNull() { } } + @Test + public void testHexToBigIntegerDecimalPath() { + assertEquals(BigInteger.valueOf(12345), ByteArray.hexToBigInteger("12345")); + assertEquals(BigInteger.ZERO, ByteArray.hexToBigInteger("0")); + assertEquals(BigInteger.ONE, ByteArray.hexToBigInteger("1")); + } + @Test public void testFromHexWithPrefix() { String input = "0x1A3F"; diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index fdbc395efe7..52eacf9f6cc 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -82,6 +82,27 @@ public void testPrintBlockToJSONEmptyTransactions() { Assert.assertNotNull(json.getJSONObject("block_header")); } + @Test + public void testPrintBlockToJSONWithTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + TransactionCapsule txCap = getTransactionCapsuleExample(); + blockCapsule.addTransaction(txCap); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertTrue(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + Assert.assertNotNull(json.getJSONObject("block_header")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(txJson.containsKey("txID")); + Assert.assertTrue(txJson.containsKey("raw_data")); + } + @Test public void testPrintTransactionList() { TransactionCapsule transactionCapsule = getTransactionCapsuleExample(); From 5b67bf6be098265d8e0bad0de11015ea85ea4a6f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 17 Apr 2026 16:43:13 +0800 Subject: [PATCH 09/17] =?UTF-8?q?fix(api):=20address=20PR=20review=20feedb?= =?UTF-8?q?ack=20=E2=80=93=20log=20levels,=20limiter=20fallback,=20null=20?= =?UTF-8?q?guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wallet: downgrade broadcast user-input validation failures from WARN to INFO (sig error, contract validate, resource insufficient, etc. are expected client errors) - RateLimiterServlet: unknown/invalid adapter falls back to default with WARN log instead of throwing TronError - ByteArray: remove redundant null checks in jsonHexToLong/jsonHexToInt - Util: simplify JSONException catch to use unqualified import - Tests: add rate limiter fallback coverage; assert block_header.raw_data presence; rename test method for clarity --- .../java/org/tron/common/utils/ByteArray.java | 4 +-- .../src/main/java/org/tron/core/Wallet.java | 16 ++++++------ .../services/http/RateLimiterServlet.java | 26 +++++++++++++------ .../org/tron/core/services/http/Util.java | 2 +- .../org/tron/common/utils/ByteArrayTest.java | 2 +- .../http/RateLimiterServletWhitelistTest.java | 22 ++++++++++++++-- .../tron/core/services/http/UtilMockTest.java | 12 ++++++--- 7 files changed, 59 insertions(+), 25 deletions(-) diff --git a/common/src/main/java/org/tron/common/utils/ByteArray.java b/common/src/main/java/org/tron/common/utils/ByteArray.java index 1577d9ea6eb..460e0174084 100644 --- a/common/src/main/java/org/tron/common/utils/ByteArray.java +++ b/common/src/main/java/org/tron/common/utils/ByteArray.java @@ -167,7 +167,7 @@ public static BigInteger hexToBigInteger(String input) { } public static long jsonHexToLong(String x) throws JsonRpcInvalidParamsException { - if (x == null || !x.startsWith("0x")) { + if (!x.startsWith("0x")) { throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); } x = x.substring(2); @@ -175,7 +175,7 @@ public static long jsonHexToLong(String x) throws JsonRpcInvalidParamsException } public static int jsonHexToInt(String x) throws Exception { - if (x == null || !x.startsWith("0x")) { + if (!x.startsWith("0x")) { throw new Exception("Incorrect hex syntax"); } x = x.substring(2); diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index b13ef560fec..dbd9918d216 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -575,41 +575,41 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(true).setCode(response_code.SUCCESS).build(); } } catch (ValidateSignatureException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.SIGERROR) .setMessage(ByteString.copyFromUtf8("Validate signature error: " + e.getMessage())) .build(); } catch (ContractValidateException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) .setMessage(ByteString.copyFromUtf8(CONTRACT_VALIDATE_ERROR + e.getMessage())) .build(); } catch (ContractExeException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) .setMessage(ByteString.copyFromUtf8("Contract execute error : " + e.getMessage())) .build(); } catch (AccountResourceInsufficientException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.BANDWITH_ERROR) .setMessage(ByteString.copyFromUtf8("Account resource insufficient error.")) .build(); } catch (DupTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8("Dup transaction.")) .build(); } catch (TaposException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TAPOS_ERROR) .setMessage(ByteString.copyFromUtf8("Tapos check error.")) .build(); } catch (TooBigTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); } catch (TransactionExpirationException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 75bb59a3624..45fb2f559ec 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -35,7 +35,7 @@ public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; static final Map> ALLOWED_ADAPTERS; - private static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); static { Map> m = new HashMap<>(); @@ -67,18 +67,28 @@ private void addRateContainer() { : item.getParams(); try { - Class c = ALLOWED_ADAPTERS.get(cName); - if (c == null) { - throw new IllegalArgumentException( - "Unknown rate limiter adapter (not in whitelist): " + cName); - } - IRateLimiter rateLimiter = c.getConstructor(String.class).newInstance(params); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); + container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); } catch (Exception e) { this.throwTronError(cName, params, name, e); } } + static IRateLimiter buildAdapter(String cName, String params, String name) throws Exception { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c != null) { + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + logger.warn("Failed to create rate limiter '{}' for servlet '{}', " + + "falling back to default. Cause: {}", cName, name, e.getMessage()); + } + } else { + logger.warn("Unknown rate limiter adapter '{}' for servlet '{}', " + + "falling back to default.", cName, name); + } + return new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); + } + private void throwTronError(String strategy, String params, String servlet, Exception e) { throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 1742a230cd2..d9da8db4ca6 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -548,7 +548,7 @@ private static String checkGetParam(HttpServletRequest request, String key) thro if (jsonObject != null) { return jsonObject.getString(key); } - } catch (com.alibaba.fastjson.JSONException e) { + } catch (JSONException e) { logger.debug("Invalid JSON input: {}", e.getMessage()); } return null; diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index 89101738d26..aefe88843ea 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -125,7 +125,7 @@ public void testHexToBigIntegerRejectsOversizedInput() { } @Test(expected = NumberFormatException.class) - public void testHexToBigIntegerRejectsEmptyString() { + public void testHexToBigIntegerEmptyStringThrowsNumberFormatException() { ByteArray.hexToBigInteger(""); } diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java index 86387f81bcb..0bbd2c38a88 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.http; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -58,7 +59,24 @@ public void testWhitelistIsUnmodifiable() { @Test public void testWhitelistSize() { - assertTrue("Whitelist should contain exactly 4 adapters", - allowedAdapters.size() == 4); + assertEquals(4, allowedAdapters.size()); + } + + @Test + public void testUnknownAdapterFallsBackToDefault() throws Exception { + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + "UnknownAdapter", "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyStrategyResolvesToDefaultAdapter() throws Exception { + // When strategy is empty in config, addRateContainer resolves to DEFAULT_ADAPTER_NAME. + // Verify buildAdapter creates a DefaultBaseQqsAdapter for that resolved name. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); } } diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 52eacf9f6cc..051f0e3baea 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -66,7 +66,9 @@ public void testPrintBlockList() { Assert.assertTrue(blockJson.containsKey("blockID")); Assert.assertTrue(blockJson.containsKey("block_header")); Assert.assertFalse(blockJson.getString("blockID").isEmpty()); - Assert.assertNotNull(blockJson.getJSONObject("block_header")); + JSONObject blockHeader = blockJson.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); } } @@ -79,7 +81,9 @@ public void testPrintBlockToJSONEmptyTransactions() { Assert.assertTrue(json.containsKey("block_header")); Assert.assertFalse(json.containsKey("transactions")); Assert.assertFalse(json.getString("blockID").isEmpty()); - Assert.assertNotNull(json.getJSONObject("block_header")); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); } @Test @@ -94,7 +98,9 @@ public void testPrintBlockToJSONWithTransactions() { Assert.assertTrue(json.containsKey("block_header")); Assert.assertTrue(json.containsKey("transactions")); Assert.assertFalse(json.getString("blockID").isEmpty()); - Assert.assertNotNull(json.getJSONObject("block_header")); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); JSONArray txArray = json.getJSONArray("transactions"); Assert.assertEquals(1, txArray.size()); From ac05ea8651f2837fa68cbe87affb9a8e96dba3f7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 17 Apr 2026 17:33:32 +0800 Subject: [PATCH 10/17] test(api): clean up test style and add missing coverage - RateLimiterServletWhitelistTest: replace assertFalse(true) with fail() - ByteArrayTest: simplify testJsonHexToInt_ValidHex to use throws instead of try/catch; add NPE tests for jsonHexToLong/jsonHexToInt null input after null-check removal - UtilMockTest: extract duplicated ServletInputStream into mockPostJsonRequest helper --- .../org/tron/common/utils/ByteArrayTest.java | 20 +++++--- .../http/RateLimiterServletWhitelistTest.java | 37 ++++----------- .../tron/core/services/http/UtilMockTest.java | 46 ++++--------------- 3 files changed, 29 insertions(+), 74 deletions(-) diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index aefe88843ea..13761bd22e4 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.fail; import static org.tron.common.utils.ByteArray.fromHex; import static org.tron.common.utils.ByteArray.jsonHexToInt; +import static org.tron.common.utils.ByteArray.jsonHexToLong; import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; @@ -97,16 +98,21 @@ public void testFromObject_SerializableObject() { } @Test - public void testJsonHexToInt_ValidHex() { - try { - int result = jsonHexToInt("0x1A"); - assertEquals(26, result); - } catch (Exception e) { - fail("Exception should not have been thrown for valid hex string."); - } + public void testJsonHexToInt_ValidHex() throws Exception { + assertEquals(26, jsonHexToInt("0x1A")); assertThrows(Exception.class, () -> ByteArray.jsonHexToInt("1A")); } + @Test(expected = NullPointerException.class) + public void testJsonHexToIntNullThrowsNpe() throws Exception { + jsonHexToInt(null); + } + + @Test(expected = NullPointerException.class) + public void testJsonHexToLongNullThrowsNpe() throws Exception { + jsonHexToLong(null); + } + @Test public void testHexToBigIntegerRejectsOversizedInput() { // 0x + 98 hex = 100 chars, at the limit diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java index 0bbd2c38a88..fe418c2f3e0 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java @@ -1,7 +1,6 @@ package org.tron.core.services.http; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -26,19 +25,14 @@ public class RateLimiterServletWhitelistTest { @Test public void testWhitelistContents() { - assertNotNull(allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); - assertNotNull(allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); - assertNotNull(allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); - assertNotNull(allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); - - assertTrue(GlobalPreemptibleAdapter.class - .isAssignableFrom(allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName()))); - assertTrue(QpsRateLimiterAdapter.class - .isAssignableFrom(allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName()))); - assertTrue(IPQPSRateLimiterAdapter.class - .isAssignableFrom(allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName()))); - assertTrue(DefaultBaseQqsAdapter.class - .isAssignableFrom(allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName()))); + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); } @Test @@ -47,21 +41,6 @@ public void testWhitelistRejectsUnknownAdapter() { assertNull(allowedAdapters.get("java.lang.Runtime")); } - @Test - public void testWhitelistIsUnmodifiable() { - try { - allowedAdapters.put("EvilAdapter", DefaultBaseQqsAdapter.class); - assertFalse("Whitelist should be unmodifiable", true); - } catch (UnsupportedOperationException e) { - // expected - } - } - - @Test - public void testWhitelistSize() { - assertEquals(4, allowedAdapters.size()); - } - @Test public void testUnknownAdapterFallsBackToDefault() throws Exception { IRateLimiter limiter = RateLimiterServlet.buildAdapter( diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 051f0e3baea..846c32cce83 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -279,48 +279,20 @@ public void testValidateParameter() { @Test public void testGetAddressWithInvalidJsonBody() throws Exception { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getMethod()).thenReturn("POST"); - Mockito.when(request.getContentType()).thenReturn("application/json"); - - // plain text, not JSON - Mockito.when(request.getInputStream()).thenReturn( - new javax.servlet.ServletInputStream() { - private final java.io.InputStream in = - new java.io.ByteArrayInputStream("not a json".getBytes()); - - @Override - public int read() throws java.io.IOException { - return in.read(); - } - - @Override - public boolean isFinished() { - return false; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setReadListener(javax.servlet.ReadListener l) { - } - }); - - byte[] address = Util.getAddress(request); - Assert.assertNull(address); + HttpServletRequest request = mockPostJsonRequest("not a json"); + Assert.assertNull(Util.getAddress(request)); } @Test public void testGetAddressWithJsonArrayBody() throws Exception { + HttpServletRequest request = mockPostJsonRequest("[\"address1\",\"address2\"]"); + Assert.assertNull(Util.getAddress(request)); + } + + private HttpServletRequest mockPostJsonRequest(String body) throws Exception { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getMethod()).thenReturn("POST"); Mockito.when(request.getContentType()).thenReturn("application/json"); - - // JSON array instead of JSON object - String body = "[\"address1\",\"address2\"]"; Mockito.when(request.getInputStream()).thenReturn( new javax.servlet.ServletInputStream() { private final java.io.InputStream in = @@ -345,9 +317,7 @@ public boolean isReady() { public void setReadListener(javax.servlet.ReadListener l) { } }); - - byte[] address = Util.getAddress(request); - Assert.assertNull(address); + return request; } @Test From 4c6df2ddeffc1e6a024f39f4666f9dfb568cb139 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 21 Apr 2026 11:17:21 +0800 Subject: [PATCH 11/17] refactor(api): move block-number length guard to JsonRpcApiUtil Address PR review (Sunny6889/waynercheung) separating concerns between low-level byte utility and API-level validation. - ByteArray.hexToBigInteger: revert length/null guards; the utility is generic and should not encode API-specific DoS policy. - JsonRpcApiUtil: new parseBlockNumber(blockNumOrTag) helper enforces MAX_BLOCK_NUM_HEX_LEN (100) and maps parse failures to JsonRpcInvalidParamsException with a stable "invalid block number" message. Comment explains the DoS motivation explicitly. - TronJsonRpcImpl / Wallet: use parseBlockNumber at five call sites, removing duplicated try/catch-and-wrap blocks. - Wallet: drop stale IncrementalMerkleTree import left over from getMerkleTreeOfBlock removal. - Util.checkGetParam: rollback JSONException catch (waynercheung MUST); malformed JSON must not be silently treated as missing keys. - ByteArrayTest: replace long2Bytes System.out.println with assertEquals; move length/null tests to JsonRpcApiUtilTest where the guard now lives. - JsonRpcApiUtilTest: new tests cover hex, decimal, max-length (100), oversized (101), null, malformed hex, and empty string. --- .../java/org/tron/common/utils/ByteArray.java | 15 ----- .../src/main/java/org/tron/core/Wallet.java | 10 +--- .../org/tron/core/services/http/Util.java | 10 +--- .../core/services/jsonrpc/JsonRpcApiUtil.java | 27 +++++++++ .../services/jsonrpc/TronJsonRpcImpl.java | 36 ++--------- .../org/tron/common/utils/ByteArrayTest.java | 32 +--------- .../tron/core/services/http/UtilMockTest.java | 44 -------------- .../services/jsonrpc/JsonRpcApiUtilTest.java | 59 +++++++++++++++++++ 8 files changed, 99 insertions(+), 134 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java diff --git a/common/src/main/java/org/tron/common/utils/ByteArray.java b/common/src/main/java/org/tron/common/utils/ByteArray.java index 460e0174084..d0ac4cbaddf 100644 --- a/common/src/main/java/org/tron/common/utils/ByteArray.java +++ b/common/src/main/java/org/tron/common/utils/ByteArray.java @@ -143,22 +143,7 @@ public static String toJsonHex(String x) { return "0x" + x; } - /** - * Max allowed length for input to hexToBigInteger. - * Covers both hex (0x + 64 = 66 for uint256) and decimal (78 for uint256) - * representations, with headroom. The exact value is not critical for - * performance — BigInteger parsing at this scale is negligible. - */ - private static final int MAX_HEX_BIG_INTEGER_LEN = 100; - public static BigInteger hexToBigInteger(String input) { - if (input == null) { - throw new IllegalArgumentException("hex input is null"); - } - if (input.length() > MAX_HEX_BIG_INTEGER_LEN) { - throw new IllegalArgumentException( - "hex input exceeds max length: " + MAX_HEX_BIG_INTEGER_LEN); - } if (input.startsWith("0x")) { return new BigInteger(input.substring(2), 16); } else { diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index dbd9918d216..0097bf2fae4 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -31,6 +31,7 @@ import static org.tron.core.config.Parameter.DatabaseConstants.MARKET_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.PROPOSAL_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.WITNESS_COUNT_LIMIT_MAX; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseEnergyFee; import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; @@ -247,7 +248,6 @@ import org.tron.protos.contract.BalanceContract.BlockBalanceTrace; import org.tron.protos.contract.BalanceContract.TransferContract; import org.tron.protos.contract.Common; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; import org.tron.protos.contract.ShieldContract.OutputPoint; import org.tron.protos.contract.ShieldContract.OutputPointInfo; @@ -743,13 +743,7 @@ public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { } else if (PENDING_STR.equalsIgnoreCase(id)) { throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); } else { - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(id).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid block number"); - } - + long blockNumber = parseBlockNumber(id).longValue(); return getBlockByNum(blockNumber); } } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index d9da8db4ca6..a0e8c77d646 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -543,13 +543,9 @@ private static String checkGetParam(HttpServletRequest request, String key) thro return null; } - try { - JSONObject jsonObject = JSON.parseObject(value); - if (jsonObject != null) { - return jsonObject.getString(key); - } - } catch (JSONException e) { - logger.debug("Invalid JSON input: {}", e.getMessage()); + JSONObject jsonObject = JSON.parseObject(value); + if (jsonObject != null) { + return jsonObject.getString(key); } return null; } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 4a60f14b534..61017155ab4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -10,6 +10,7 @@ import com.google.common.primitives.Longs; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; @@ -515,6 +516,32 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { return -1; } + /** + * Max allowed length for a JSON-RPC block number hex/decimal input. + * API-level DoS guard: rejects pathological inputs before BigInteger parsing, + * whose cost grows quadratically with length. Covers hex (0x + 64 chars for + * uint256) and decimal (78 chars for uint256) representations with headroom. + */ + private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + + private static final String BLOCK_NUM_ERROR = "invalid block number"; + + /** + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a BigInteger, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit. + */ + public static BigInteger parseBlockNumber(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (blockNumOrTag == null || blockNumOrTag.length() > MAX_BLOCK_NUM_HEX_LEN) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return ByteArray.hexToBigInteger(blockNumOrTag); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + } + public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) throws JsonRpcInvalidParamsException { if (PENDING_STR.equalsIgnoreCase(blockNumOrTag)) { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index de939bdfff4..ce3adf42d7d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -8,6 +8,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.alibaba.fastjson.JSON; @@ -409,12 +410,7 @@ public String getTrxBalance(String address, String blockNumOrTag) } return ByteArray.toJsonHex(balance); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -558,12 +554,7 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -589,12 +580,7 @@ public String getABIOfSmartContract(String contractAddress, String blockNumOrTag } } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -971,12 +957,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + long blockNumber = parseBlockNumber(blockNumOrTag).longValue(); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); @@ -1014,12 +995,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) return call(addressData, contractAddressData, transactionCall.parseValue(), ByteArray.fromHexString(transactionCall.getData())); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index 13761bd22e4..243fcc69fbb 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.tron.common.utils.ByteArray.fromHex; import static org.tron.common.utils.ByteArray.jsonHexToInt; import static org.tron.common.utils.ByteArray.jsonHexToLong; @@ -43,8 +42,8 @@ public void testToHexString() { public void long2Bytes() { long a = 0x123456; byte[] bb = ByteArray.fromLong(a); - System.out.println(bb[6]); - System.out.println(bb[7]); + assertEquals(0x34, bb[6]); + assertEquals(0x56, bb[7]); } @Test @@ -113,38 +112,11 @@ public void testJsonHexToLongNullThrowsNpe() throws Exception { jsonHexToLong(null); } - @Test - public void testHexToBigIntegerRejectsOversizedInput() { - // 0x + 98 hex = 100 chars, at the limit - String maxValid = "0x" + new String(new char[98]).replace('\0', 'f'); - assertEquals(100, maxValid.length()); - ByteArray.hexToBigInteger(maxValid); // should not throw - - // 0x + 99 hex = 101 chars, exceeds limit - String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); - try { - ByteArray.hexToBigInteger(tooLong); - fail("expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("exceeds max length")); - } - } - @Test(expected = NumberFormatException.class) public void testHexToBigIntegerEmptyStringThrowsNumberFormatException() { ByteArray.hexToBigInteger(""); } - @Test - public void testHexToBigIntegerRejectsNull() { - try { - ByteArray.hexToBigInteger(null); - fail("expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("null")); - } - } - @Test public void testHexToBigIntegerDecimalPath() { assertEquals(BigInteger.valueOf(12345), ByteArray.hexToBigInteger("12345")); diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 846c32cce83..592571b25c6 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -7,7 +7,6 @@ import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.List; -import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -277,49 +276,6 @@ public void testValidateParameter() { ); } - @Test - public void testGetAddressWithInvalidJsonBody() throws Exception { - HttpServletRequest request = mockPostJsonRequest("not a json"); - Assert.assertNull(Util.getAddress(request)); - } - - @Test - public void testGetAddressWithJsonArrayBody() throws Exception { - HttpServletRequest request = mockPostJsonRequest("[\"address1\",\"address2\"]"); - Assert.assertNull(Util.getAddress(request)); - } - - private HttpServletRequest mockPostJsonRequest(String body) throws Exception { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getMethod()).thenReturn("POST"); - Mockito.when(request.getContentType()).thenReturn("application/json"); - Mockito.when(request.getInputStream()).thenReturn( - new javax.servlet.ServletInputStream() { - private final java.io.InputStream in = - new java.io.ByteArrayInputStream(body.getBytes()); - - @Override - public int read() throws java.io.IOException { - return in.read(); - } - - @Override - public boolean isFinished() { - return false; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setReadListener(javax.servlet.ReadListener l) { - } - }); - return request; - } - @Test public void testGetJsonString() { String str = ""; diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java new file mode 100644 index 00000000000..5c29a3f728b --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -0,0 +1,59 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.math.BigInteger; +import org.junit.Test; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public class JsonRpcApiUtilTest { + + @Test + public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(BigInteger.valueOf(0x1a), JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(BigInteger.ZERO, JsonRpcApiUtil.parseBlockNumber("0x0")); + } + + @Test + public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { + assertEquals(BigInteger.valueOf(12345), JsonRpcApiUtil.parseBlockNumber("12345")); + } + + @Test + public void parseBlockNumberAcceptsMaxLength() throws JsonRpcInvalidParamsException { + // 0x + 98 hex chars = 100 chars total, at the limit + String maxValid = "0x" + new String(new char[98]).replace('\0', 'f'); + assertEquals(100, maxValid.length()); + JsonRpcApiUtil.parseBlockNumber(maxValid); + } + + @Test + public void parseBlockNumberRejectsOversized() { + // 101 chars exceeds the 100-char limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(null)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xGG")); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsEmpty() { + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("")); + } +} From 3d41716058b56f21a2bb5764e392261f44b9f458 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 21 Apr 2026 12:13:56 +0800 Subject: [PATCH 12/17] fix(api): restore fail-fast for unknown rate limiter adapter Pre-whitelist code used Class.forName(ADAPTER_PREFIX + cName), so a misspelled or unknown strategy name raised ClassNotFoundException, which addRateContainer's catch lifted into TronError and aborted Spring startup. The whitelist refactor softened this to warn+fallback and masked misconfigured nodes. Restore original semantics: - buildAdapter throws TronError (via throwTronError) when the name is not in the whitelist or the constructor fails; the no-fallback behavior matches the original Class.forName chain exactly. - addRateContainer branches strictly on item == null (original behavior) rather than per-field null/empty defaulting; a configured but empty strategy name is a configuration bug and fails fast. - Drop the now-invalid `throws Exception` on buildAdapter; make throwTronError static and return TronError so callers use `throw throwTronError(...)` for compiler reachability. Tests: - Rename RateLimiterServletWhitelistTest -> RateLimiterServletTest to match the Test convention used elsewhere (UtilTest, UpdateAccountServletTest, etc.). - Replace fallback-assertion test with testUnknownAdapterThrowsTronError asserting RATE_LIMITER_INIT and servlet/adapter name in message. - Rename the DEFAULT_ADAPTER_NAME happy-path test for clarity. - Add testEmptyAdapterNameThrowsTronError covering the empty-strategy fail-fast branch reached via item != null with empty getStrategy(). - Add testBuildsEachWhitelistedAdapter exercising newInstance(String) for QpsRateLimiterAdapter, IPQPSRateLimiterAdapter, and GlobalPreemptibleAdapter so a signature or strategy-class break on any entry fails in CI instead of at node startup. --- .../services/http/RateLimiterServlet.java | 48 +++++----- .../services/http/RateLimiterServletTest.java | 93 +++++++++++++++++++ .../http/RateLimiterServletWhitelistTest.java | 61 ------------ 3 files changed, 118 insertions(+), 84 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java delete mode 100644 framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 45fb2f559ec..c7ff63aa507 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -55,42 +55,44 @@ public abstract class RateLimiterServlet extends HttpServlet { @PostConstruct private void addRateContainer() { - RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() - .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); final String name = getClass().getSimpleName(); + RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() + .getRateLimiterInitialization().getHttpMap().get(name); - String cName = (item == null || Strings.isNullOrEmpty(item.getStrategy())) - ? DEFAULT_ADAPTER_NAME - : item.getStrategy(); - String params = (item == null || Strings.isNullOrEmpty(item.getParams())) - ? QpsStrategy.DEFAULT_QPS_PARAM - : item.getParams(); + String cName; + String params; + if (item == null) { + cName = DEFAULT_ADAPTER_NAME; + params = QpsStrategy.DEFAULT_QPS_PARAM; + } else { + cName = item.getStrategy(); + params = item.getParams(); + } try { container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); } catch (Exception e) { - this.throwTronError(cName, params, name, e); + throw throwTronError(cName, params, name, e); } } - static IRateLimiter buildAdapter(String cName, String params, String name) throws Exception { + static IRateLimiter buildAdapter(String cName, String params, String name) { Class c = ALLOWED_ADAPTERS.get(cName); - if (c != null) { - try { - return c.getConstructor(String.class).newInstance(params); - } catch (Exception e) { - logger.warn("Failed to create rate limiter '{}' for servlet '{}', " - + "falling back to default. Cause: {}", cName, name, e.getMessage()); - } - } else { - logger.warn("Unknown rate limiter adapter '{}' for servlet '{}', " - + "falling back to default.", cName, name); + if (c == null) { + throw throwTronError(cName, params, name, + new IllegalArgumentException("unknown rate limiter adapter; allowed=" + + ALLOWED_ADAPTERS.keySet())); + } + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + throw throwTronError(cName, params, name, e); } - return new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); } - private void throwTronError(String strategy, String params, String servlet, Exception e) { - throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + private static TronError throwTronError(String strategy, String params, String servlet, + Exception e) { + return new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", e, TronError.ErrCode.RATE_LIMITER_INIT); } diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java new file mode 100644 index 00000000000..4ae76f85dfb --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -0,0 +1,93 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies RateLimiterServlet's adapter resolution: strict whitelist + * (no Class.forName arbitrary class loading), fail-fast on unknown or + * empty names, and successful construction of every whitelisted adapter. + */ +public class RateLimiterServletTest { + + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; + + @Test + public void testWhitelistContents() { + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testUnknownAdapterThrowsTronError() { + // Fail-fast parity with the pre-whitelist Class.forName behavior: an unknown + // adapter name raises TronError from @PostConstruct so Spring startup aborts + // rather than silently masking a misconfigured node. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("UnknownAdapter", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + assertTrue(e.getMessage().contains("UnknownAdapter")); + assertTrue(e.getMessage().contains("TestServlet")); + } + + @Test + public void testDefaultAdapterNameBuildsDefaultBaseQqsAdapter() { + // When no config entry exists for a servlet, addRateContainer passes + // DEFAULT_ADAPTER_NAME to buildAdapter; verify it resolves to + // DefaultBaseQqsAdapter. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyAdapterNameThrowsTronError() { + // Fail-fast parity with original: a configured-but-empty strategy name is + // a configuration bug and must not be silently replaced by the default. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + } + + @Test + public void testBuildsEachWhitelistedAdapter() { + // Exercises the newInstance(String) constructor path for every whitelisted + // adapter so a signature/strategy-class break on any entry fails here + // instead of at node startup. + assertTrue(RateLimiterServlet.buildAdapter( + QpsRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof QpsRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + IPQPSRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof IPQPSRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + GlobalPreemptibleAdapter.class.getSimpleName(), "permit=1", "TestServlet") + instanceof GlobalPreemptibleAdapter); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java deleted file mode 100644 index fe418c2f3e0..00000000000 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.tron.core.services.http; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.Map; -import org.junit.Test; -import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; -import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; -import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; -import org.tron.core.services.ratelimiter.adapter.IRateLimiter; -import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; - -/** - * Verifies that RateLimiterServlet uses a strict whitelist - * instead of Class.forName(), preventing arbitrary class loading - * via a tampered config file. - */ -public class RateLimiterServletWhitelistTest { - - private static final Map> allowedAdapters = - RateLimiterServlet.ALLOWED_ADAPTERS; - - @Test - public void testWhitelistContents() { - assertEquals(GlobalPreemptibleAdapter.class, - allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); - assertEquals(QpsRateLimiterAdapter.class, - allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); - assertEquals(IPQPSRateLimiterAdapter.class, - allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); - assertEquals(DefaultBaseQqsAdapter.class, - allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); - } - - @Test - public void testWhitelistRejectsUnknownAdapter() { - assertNull(allowedAdapters.get("EvilAdapter")); - assertNull(allowedAdapters.get("java.lang.Runtime")); - } - - @Test - public void testUnknownAdapterFallsBackToDefault() throws Exception { - IRateLimiter limiter = RateLimiterServlet.buildAdapter( - "UnknownAdapter", "qps=100", "TestServlet"); - assertNotNull(limiter); - assertTrue(limiter instanceof DefaultBaseQqsAdapter); - } - - @Test - public void testEmptyStrategyResolvesToDefaultAdapter() throws Exception { - // When strategy is empty in config, addRateContainer resolves to DEFAULT_ADAPTER_NAME. - // Verify buildAdapter creates a DefaultBaseQqsAdapter for that resolved name. - IRateLimiter limiter = RateLimiterServlet.buildAdapter( - RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); - assertNotNull(limiter); - assertTrue(limiter instanceof DefaultBaseQqsAdapter); - } -} From f200e1f134a26a3a1145e95a8a081af9ce071fd0 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 21 Apr 2026 15:22:07 +0800 Subject: [PATCH 13/17] test(api): verify visible flag in printBlockToJSON Add two regression tests that pin the optimized printBlockToJSON path against the visible=true/false semantics of the prior implementation, which the field-by-field assembly now has to thread through JsonFormat explicitly. - testPrintBlockToJSONVisibleFalse: assert structural invariants (blockID, block_header, transactions) under visible=false, mirroring the existing visible=true cases. - testPrintBlockToJSONVisibleFlagAffectsAddressEncoding: build a block with a mainnet 0x41-prefixed witness_address and assert - blockID is identical under either flag (byte-level hash), - block_header JSON differs between the two (witness_address is re-encoded), and - visible=true renders witness_address as Base58 ("T...") while visible=false does not. --- .../tron/core/services/http/UtilMockTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 592571b25c6..fb5919d1038 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -108,6 +108,63 @@ public void testPrintBlockToJSONWithTransactions() { Assert.assertTrue(txJson.containsKey("raw_data")); } + @Test + public void testPrintBlockToJSONVisibleFalse() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + TransactionCapsule txCap = getTransactionCapsuleExample(); + blockCapsule.addTransaction(txCap); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), false); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertTrue(json.containsKey("transactions")); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(txJson.containsKey("txID")); + Assert.assertTrue(txJson.containsKey("raw_data")); + } + + @Test + public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { + // Pins the optimized printBlockToJSON against the prior behavior: the + // visible flag must still thread through to JsonFormat so address-bearing + // fields switch encoding while byte-identity fields stay stable. + ByteString witnessAddress = ByteString.copyFrom( + ByteArray.fromHexString("41548794500882809695a8a687866e76d4271a1abc")); + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), witnessAddress); + + JSONObject visible = Util.printBlockToJSON(blockCapsule.getInstance(), true); + JSONObject hidden = Util.printBlockToJSON(blockCapsule.getInstance(), false); + + // blockID is derived from raw bytes; identical under either flag. + Assert.assertEquals(visible.getString("blockID"), hidden.getString("blockID")); + + // Overall block_header must differ because witness_address is re-encoded. + String headerVisible = visible.getJSONObject("block_header").toJSONString(); + String headerHidden = hidden.getJSONObject("block_header").toJSONString(); + Assert.assertNotEquals(headerVisible, headerHidden); + + // visible=true renders a mainnet address as Base58 starting with 'T'. + String witnessVisible = visible.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessVisible); + Assert.assertTrue("visible=true witness_address should be Base58 ('T...'), got: " + + witnessVisible, witnessVisible.startsWith("T")); + + // visible=false keeps witness_address in raw (non-Base58) form. + String witnessHidden = hidden.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessHidden); + Assert.assertNotEquals(witnessVisible, witnessHidden); + } + @Test public void testPrintTransactionList() { TransactionCapsule transactionCapsule = getTransactionCapsuleExample(); From f4f7412ff84c8b5ceb61d403dc8ff860d085ba4b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 21 Apr 2026 15:43:28 +0800 Subject: [PATCH 14/17] test(api): revert goalless ByteArrayTest changes out of PR scope The hexToBigInteger length guard and tests were moved to JsonRpcApiUtil in 4c6df2dde; ByteArray.java net diff is already zero. Remaining ByteArrayTest edits (System.out.println -> assertEquals cleanup, new null/empty/decimal tests for unchanged ByteArray methods) are drive-by refactors unrelated to the PR goal. Restore the file to develop. --- .../org/tron/common/utils/ByteArrayTest.java | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java index 243fcc69fbb..c0db8c4b418 100644 --- a/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java +++ b/framework/src/test/java/org/tron/common/utils/ByteArrayTest.java @@ -20,11 +20,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.tron.common.utils.ByteArray.fromHex; import static org.tron.common.utils.ByteArray.jsonHexToInt; -import static org.tron.common.utils.ByteArray.jsonHexToLong; -import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; @@ -42,8 +41,8 @@ public void testToHexString() { public void long2Bytes() { long a = 0x123456; byte[] bb = ByteArray.fromLong(a); - assertEquals(0x34, bb[6]); - assertEquals(0x56, bb[7]); + System.out.println(bb[6]); + System.out.println(bb[7]); } @Test @@ -97,33 +96,16 @@ public void testFromObject_SerializableObject() { } @Test - public void testJsonHexToInt_ValidHex() throws Exception { - assertEquals(26, jsonHexToInt("0x1A")); + public void testJsonHexToInt_ValidHex() { + try { + int result = jsonHexToInt("0x1A"); + assertEquals(26, result); + } catch (Exception e) { + fail("Exception should not have been thrown for valid hex string."); + } assertThrows(Exception.class, () -> ByteArray.jsonHexToInt("1A")); } - @Test(expected = NullPointerException.class) - public void testJsonHexToIntNullThrowsNpe() throws Exception { - jsonHexToInt(null); - } - - @Test(expected = NullPointerException.class) - public void testJsonHexToLongNullThrowsNpe() throws Exception { - jsonHexToLong(null); - } - - @Test(expected = NumberFormatException.class) - public void testHexToBigIntegerEmptyStringThrowsNumberFormatException() { - ByteArray.hexToBigInteger(""); - } - - @Test - public void testHexToBigIntegerDecimalPath() { - assertEquals(BigInteger.valueOf(12345), ByteArray.hexToBigInteger("12345")); - assertEquals(BigInteger.ZERO, ByteArray.hexToBigInteger("0")); - assertEquals(BigInteger.ONE, ByteArray.hexToBigInteger("1")); - } - @Test public void testFromHexWithPrefix() { String input = "0x1A3F"; From db91a726acb0a25fbb7747cc1fadb11490a13f27 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 21 Apr 2026 15:43:39 +0800 Subject: [PATCH 15/17] test(api): harden printBlockToJSON tests against drift Coverage added/consolidated around the hand-rolled printBlockToJSON from 09804886a so future regressions surface in tests, not at runtime. - Merge testPrintBlockToJSONVisibleFalse into testPrintBlockToJSONWithTransactions via a boolean[]{true,false} loop; flag-driven encoding difference remains covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. - testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl: pin parity with the prior JsonFormat.printToString(block, ...) output for the transactions-key presence in both empty and non-empty cases. - testPrintBlockToJSONCoversAllProtoTopLevelFields: reflect over Block's descriptor to fail fast if a new top-level proto field is added without extending the hand-rolled assembler. --- .../tron/core/services/http/UtilMockTest.java | 117 ++++++++++++------ 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index fb5919d1038..7c05e0e9cfe 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -3,10 +3,13 @@ import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -87,47 +90,30 @@ public void testPrintBlockToJSONEmptyTransactions() { @Test public void testPrintBlockToJSONWithTransactions() { - BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - TransactionCapsule txCap = getTransactionCapsuleExample(); - blockCapsule.addTransaction(txCap); - - JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); - Assert.assertTrue(json.containsKey("blockID")); - Assert.assertTrue(json.containsKey("block_header")); - Assert.assertTrue(json.containsKey("transactions")); - Assert.assertFalse(json.getString("blockID").isEmpty()); - JSONObject blockHeader = json.getJSONObject("block_header"); - Assert.assertNotNull(blockHeader); - Assert.assertTrue(blockHeader.containsKey("raw_data")); - - JSONArray txArray = json.getJSONArray("transactions"); - Assert.assertEquals(1, txArray.size()); - JSONObject txJson = txArray.getJSONObject(0); - Assert.assertTrue(txJson.containsKey("txID")); - Assert.assertTrue(txJson.containsKey("raw_data")); - } - - @Test - public void testPrintBlockToJSONVisibleFalse() { - BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - TransactionCapsule txCap = getTransactionCapsuleExample(); - blockCapsule.addTransaction(txCap); - - JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), false); - Assert.assertTrue(json.containsKey("blockID")); - Assert.assertTrue(json.containsKey("block_header")); - Assert.assertTrue(json.containsKey("transactions")); - JSONObject blockHeader = json.getJSONObject("block_header"); - Assert.assertNotNull(blockHeader); - Assert.assertTrue(blockHeader.containsKey("raw_data")); - - JSONArray txArray = json.getJSONArray("transactions"); - Assert.assertEquals(1, txArray.size()); - JSONObject txJson = txArray.getJSONObject(0); - Assert.assertTrue(txJson.containsKey("txID")); - Assert.assertTrue(txJson.containsKey("raw_data")); + // Structural invariants must hold under either visible flag; the flag-driven + // encoding difference is covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. + for (boolean visible : new boolean[]{true, false}) { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + blockCapsule.addTransaction(getTransactionCapsuleExample()); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), visible); + + String msg = "visible=" + visible; + Assert.assertTrue(msg, json.containsKey("blockID")); + Assert.assertTrue(msg, json.containsKey("block_header")); + Assert.assertTrue(msg, json.containsKey("transactions")); + Assert.assertFalse(msg, json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(msg, blockHeader); + Assert.assertTrue(msg, blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(msg, 1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(msg, txJson.containsKey("txID")); + Assert.assertTrue(msg, txJson.containsKey("raw_data")); + } } @Test @@ -165,6 +151,55 @@ public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { Assert.assertNotEquals(witnessVisible, witnessHidden); } + @Test + public void testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl() { + // Legacy impl produced JSON via JsonFormat.printToString(block, selfType), + // which omits repeated fields when empty. New impl mirrors that with an + // explicit isEmpty() guard. Pin parity using JsonFormat output as ground + // truth so a future refactor can't quietly start emitting "transactions": [] + // (or stop emitting the key when non-empty). + BlockCapsule empty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + assertTransactionsKeyMatchesLegacy(empty.getInstance(), false); + + BlockCapsule nonEmpty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + nonEmpty.addTransaction(getTransactionCapsuleExample()); + assertTransactionsKeyMatchesLegacy(nonEmpty.getInstance(), true); + } + + private static void assertTransactionsKeyMatchesLegacy(Protocol.Block block, + boolean expectTransactionsKey) { + JSONObject legacy = JSONObject.parseObject(JsonFormat.printToString(block, true)); + Assert.assertEquals("legacy JsonFormat parity broken — proto behavior changed?", + expectTransactionsKey, legacy.containsKey("transactions")); + + JSONObject actual = Util.printBlockToJSON(block, true); + Assert.assertEquals("new impl diverged from legacy on 'transactions' key presence", + expectTransactionsKey, actual.containsKey("transactions")); + } + + @Test + public void testPrintBlockToJSONCoversAllProtoTopLevelFields() { + // Guards against proto field drift: the old impl delegated to JsonFormat on + // the whole Block message, so any new top-level Block field appeared + // automatically. The new impl hand-assembles the JSON, so a future proto + // field would be silently dropped. Reflect over Block's descriptor and + // assert every declared top-level field is handled. + Map protoFieldToJsonKey = new HashMap<>(); + protoFieldToJsonKey.put("block_header", "block_header"); + // "transactions" is present only when non-empty; parity verified in + // testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl. + protoFieldToJsonKey.put("transactions", "transactions"); + + for (Descriptors.FieldDescriptor f : Protocol.Block.getDescriptor().getFields()) { + Assert.assertTrue( + "Block proto field '" + f.getName() + "' is not handled by printBlockToJSON. " + + "If you added a new top-level field, extend printBlockToJSON and this test.", + protoFieldToJsonKey.containsKey(f.getName())); + } + } + @Test public void testPrintTransactionList() { TransactionCapsule transactionCapsule = getTransactionCapsuleExample(); From a437574a743dd4616f68a5efb24034157f5692ae Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 22 Apr 2026 11:03:58 +0800 Subject: [PATCH 16/17] fix(api): validate block number range in parseBlockNumber parseBlockNumber previously returned BigInteger, letting callers narrow with .longValue() and silently wrap past Long.MAX_VALUE. Return long directly and enforce two invariants at the API boundary so corrupted inputs never reach downstream block lookups: - signum check rejects negative inputs (both "-1" and "0x-1"), a protocol-level constraint block numbers can never violate - longValueExact rejects values that overflow signed 64-bit, including uint64 wraparounds like 0xffffffffffffffff Callers in Wallet and TronJsonRpcImpl drop the obsolete .longValue() call; the other three call sites already discarded the return value. Regression tests cover negatives, 0x7fffffffffffffff (max long), 0x8000000000000000 (just past), and 0xffffffffffffffff (uint64). --- .../src/main/java/org/tron/core/Wallet.java | 2 +- .../core/services/jsonrpc/JsonRpcApiUtil.java | 19 ++++++++-- .../services/jsonrpc/TronJsonRpcImpl.java | 2 +- .../services/jsonrpc/JsonRpcApiUtilTest.java | 37 ++++++++++++++----- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0097bf2fae4..d9f3ee7406b 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -743,7 +743,7 @@ public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { } else if (PENDING_STR.equalsIgnoreCase(id)) { throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); } else { - long blockNumber = parseBlockNumber(id).longValue(); + long blockNumber = parseBlockNumber(id); return getBlockByNum(blockNumber); } } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 61017155ab4..c3aca97e945 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -527,19 +527,30 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { private static final String BLOCK_NUM_ERROR = "invalid block number"; /** - * Parse a JSON-RPC block number (hex "0x..." or decimal) into a BigInteger, - * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit. + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit, rejecting + * negative values, and rejecting values that overflow a signed 64-bit + * block number. */ - public static BigInteger parseBlockNumber(String blockNumOrTag) + public static long parseBlockNumber(String blockNumOrTag) throws JsonRpcInvalidParamsException { if (blockNumOrTag == null || blockNumOrTag.length() > MAX_BLOCK_NUM_HEX_LEN) { throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } + BigInteger value; try { - return ByteArray.hexToBigInteger(blockNumOrTag); + value = ByteArray.hexToBigInteger(blockNumOrTag); } catch (Exception e) { throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } + if (value.signum() < 0) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return value.longValueExact(); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } } public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index ce3adf42d7d..dd570dbd487 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -957,7 +957,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber = parseBlockNumber(blockNumOrTag).longValue(); + long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java index 5c29a3f728b..6aaeea2cc4e 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; -import java.math.BigInteger; import org.junit.Test; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; @@ -11,21 +10,41 @@ public class JsonRpcApiUtilTest { @Test public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { - assertEquals(BigInteger.valueOf(0x1a), JsonRpcApiUtil.parseBlockNumber("0x1a")); - assertEquals(BigInteger.ZERO, JsonRpcApiUtil.parseBlockNumber("0x0")); + assertEquals(0x1aL, JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(0L, JsonRpcApiUtil.parseBlockNumber("0x0")); } @Test public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { - assertEquals(BigInteger.valueOf(12345), JsonRpcApiUtil.parseBlockNumber("12345")); + assertEquals(12345L, JsonRpcApiUtil.parseBlockNumber("12345")); } @Test - public void parseBlockNumberAcceptsMaxLength() throws JsonRpcInvalidParamsException { - // 0x + 98 hex chars = 100 chars total, at the limit - String maxValid = "0x" + new String(new char[98]).replace('\0', 'f'); - assertEquals(100, maxValid.length()); - JsonRpcApiUtil.parseBlockNumber(maxValid); + public void parseBlockNumberAcceptsMaxLongValue() throws JsonRpcInvalidParamsException { + assertEquals(Long.MAX_VALUE, + JsonRpcApiUtil.parseBlockNumber("0x7fffffffffffffff")); + } + + @Test + public void parseBlockNumberRejectsNegative() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("-1")); + assertEquals("invalid block number", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x-1")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOverflow() { + // 2^64 - 1: fits uint64 but overflows signed long + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xffffffffffffffff")); + assertEquals("invalid block number", e1.getMessage()); + // 2^63: just past Long.MAX_VALUE + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x8000000000000000")); + assertEquals("invalid block number", e2.getMessage()); } @Test From 0fdf5798266588c9be630f1bdf90a1fc44dbe63e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 22 Apr 2026 11:04:04 +0800 Subject: [PATCH 17/17] style(api): use Arrays.asList for typed adapter whitelist Replace the raw Class[] literal (which the compiler cannot type-check and emits an unchecked-conversion warning for) with Arrays.asList so the element type Class is inferred explicitly. No behavior change; ALLOWED_ADAPTERS keeps the same four entries. --- .../tron/core/services/http/RateLimiterServlet.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index c7ff63aa507..1fadc7a6e54 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,8 +3,10 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; @@ -38,13 +40,13 @@ public abstract class RateLimiterServlet extends HttpServlet { static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); static { - Map> m = new HashMap<>(); - for (Class c : new Class[]{ + List> adapters = Arrays.asList( GlobalPreemptibleAdapter.class, QpsRateLimiterAdapter.class, IPQPSRateLimiterAdapter.class, - DefaultBaseQqsAdapter.class - }) { + DefaultBaseQqsAdapter.class); + Map> m = new HashMap<>(); + for (Class c : adapters) { m.put(c.getSimpleName(), c); } ALLOWED_ADAPTERS = Collections.unmodifiableMap(m);