From 17f6208667ccd5dace0f75d60cacc73d1d5498aa Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 8 Jun 2026 17:05:04 +0800 Subject: [PATCH 1/7] fix(jsonrpc): harden RPC/HTTP parameter validation Add length/format guards across the JSON-RPC and HTTP address-handling paths so malformed or oversized parameters are rejected early with a clear -32602 / error response, instead of triggering O(n^2) work or leaking NPE / Internal errors. - Commons.decodeFromBase58Check: reject non-34-char input before the O(n^2) Base58 decode; single funnel covering every HTTP address input. - JsonFormat.unescapeBytesSelfType: turn an invalid Base58 address (null or illegal-char) into a clear "invalid address" error instead of a bare ByteString.copyFrom(null) NPE; covers all merge-path address fields. - JsonRpcApiUtil.addressCompatibleToByteArray: cap length at ADDRESS_SIZE + 2 (the +2 keeps 0x-prefixed 21-byte addresses valid) before fromHexString. - JsonRpcApiUtil.parseBlockNumber: tighten MAX_BLOCK_NUM_HEX_LEN 100 -> 20 and route the (String, Wallet) overload through the single-arg parser so it also gets the length cap, overflow (longValueExact) and negative checks. - TronJsonRpcImpl.getStorageAt: bound the storage key and parse it before the contract lookup; wrap DataWord parsing so a >32-byte key returns invalid-params instead of an Internal error. - Hoist hashToByteArray/HASH_REGEX into JsonRpcApiUtil so LogFilterWrapper's blockHash gets the same 32-byte-hash validation. --- .../java/org/tron/common/utils/Commons.java | 10 ++++ .../tron/core/services/http/JsonFormat.java | 14 +++++- .../core/services/jsonrpc/JsonRpcApiUtil.java | 42 +++++++++++++--- .../services/jsonrpc/TronJsonRpcImpl.java | 36 +++++++------- .../jsonrpc/filters/LogFilterWrapper.java | 5 +- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 48 +++++++++++++++++-- .../services/http/GetAccountServletTest.java | 32 +++++++++++++ .../services/http/GetRewardServletTest.java | 31 ++++++++++++ .../services/jsonrpc/JsonRpcApiUtilTest.java | 27 ++++++++++- 9 files changed, 211 insertions(+), 34 deletions(-) diff --git a/chainbase/src/main/java/org/tron/common/utils/Commons.java b/chainbase/src/main/java/org/tron/common/utils/Commons.java index b121e84ecfe..634e2b34384 100644 --- a/chainbase/src/main/java/org/tron/common/utils/Commons.java +++ b/chainbase/src/main/java/org/tron/common/utils/Commons.java @@ -21,6 +21,8 @@ public class Commons { public static final int ASSET_ISSUE_COUNT_LIMIT_MAX = 1000; + public static final int BASE58_ADDRESS_LENGTH = 34; + public static byte[] decode58Check(String input) { byte[] decodeCheck = Base58.decode(input); if (decodeCheck.length <= 4) { @@ -41,11 +43,19 @@ public static byte[] decode58Check(String input) { return null; } + /** + * Decode a Base58Check address string to its 21-byte form. + */ public static byte[] decodeFromBase58Check(String addressBase58) { if (StringUtils.isEmpty(addressBase58)) { logger.warn("Warning: Address is empty !!"); return null; } + if (addressBase58.length() != BASE58_ADDRESS_LENGTH) { + logger.warn("Warning: invalid Base58 address length: {} ,expected {} !!", + addressBase58.length(), BASE58_ADDRESS_LENGTH); + return null; + } byte[] address = decode58Check(addressBase58); if (address == null) { return null; diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 1dab6c7b941..e0f00181817 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -1313,7 +1313,19 @@ static ByteString unescapeBytesSelfType(String input, final String fliedName) throws InvalidEscapeSequence { //Address base58 -> ByteString if (HttpSelfFormatFieldName.isAddressFormat(fliedName)) { - return ByteString.copyFrom(Commons.decodeFromBase58Check(input)); + byte[] addressBytes; + try { + addressBytes = Commons.decodeFromBase58Check(input); + } catch (IllegalArgumentException e) { + // Base58.decode throw + addressBytes = null; + } + if (addressBytes == null) { + // empty / wrong-length / bad-checksum / illegal chars -> all invalid addresses; throw a + // clear error instead of letting ByteString.copyFrom(null) throw a bare NPE. + throw new InvalidEscapeSequence("invalid address for field: " + fliedName); + } + return ByteString.copyFrom(addressBytes); } //Normal String -> ByteString 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 6a0957d62d2..bf7fc18119c 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 @@ -8,6 +8,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.encoders.Hex; @@ -395,6 +396,11 @@ public static long getUnfreezeAssetAmount(byte[] addressBytes, Wallet wallet) { */ public static byte[] addressCompatibleToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { + // ADDRESS_SIZE (42) is the hex length of a 21-byte address; +2 leaves room for the optional + // "0x" prefix, so a 0x-prefixed 21-byte address (0x41..., 44 chars) is still accepted. + if (hexAddress == null || hexAddress.length() > DecodeUtil.ADDRESS_SIZE + 2) { + throw new JsonRpcInvalidParamsException("invalid address hash value"); + } byte[] addressByte; try { addressByte = ByteArray.fromHexString(hexAddress); @@ -415,6 +421,26 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) return addressByte; } + /** Matches a 32-byte hash hex string: optional 0x prefix + 64 hex chars (also caps length). */ + public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; + + /** + * Convert a hash hex string (optional 0x prefix) to a byte array, validating + * format and length via {@link #HASH_REGEX} first. + */ + public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { + if (!Pattern.matches(HASH_REGEX, hash)) { + throw new JsonRpcInvalidParamsException("invalid hash value"); + } + byte[] bHash; + try { + bHash = ByteArray.fromHexString(hash); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(e.getMessage()); + } + return bHash; + } + /** * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. */ @@ -603,10 +629,11 @@ public static long parseBlockTag(String tag, Wallet wallet) /** * 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. + * whose cost grows quadratically with length. A block number fits a signed long, + * so the longest valid input is 19 chars (decimal Long.MAX_VALUE) or 18 (0x + 16 + * hex); 20 leaves a small margin. */ - private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + private static final int MAX_BLOCK_NUM_HEX_LEN = 20; /** * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, @@ -636,16 +663,17 @@ public static long parseBlockNumber(String blockNum) } /** - * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. - * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use - * isBlockTag/parseBlockTag and handle hex separately with hexToBigInteger. + * Parse a block tag, or a 0x-prefixed hex block number. */ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) throws JsonRpcInvalidParamsException { if (isBlockTag(blockNumOrTag)) { return parseBlockTag(blockNumOrTag, wallet); } - return ByteArray.jsonHexToLong(blockNumOrTag); + if (!blockNumOrTag.startsWith("0x")) { + throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); + } + return parseBlockNumber(blockNumOrTag); } public static String generateFilterId() { 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 4d919b81ece..f536a47c6fb 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 @@ -3,14 +3,15 @@ import static org.tron.core.Wallet.CONTRACT_VALIDATE_ERROR; import static org.tron.core.services.http.Util.setTransactionExtraData; import static org.tron.core.services.http.Util.setTransactionPermissionId; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.BLOCK_NUM_ERROR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.FINALIZED_STR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.HASH_REGEX; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.generateFilterId; 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.hashToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; @@ -157,7 +158,11 @@ public enum RequestSource { private final Map blockFilter2ResultSolidity = new ConcurrentHashMap<>(); - public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; + // Storage key is a 32-byte word: 64 hex chars + optional "0x" prefix = 66 max. + // Reject oversized input before fromHexString / new DataWord, which would otherwise + // throw a RuntimeException whose message embeds the whole hex (amplifying log output) + // and surface as a -32603 Internal error instead of -32602 invalid params. + public static final int MAX_STORAGE_KEY_HEX_LEN = 66; public static final String INVALID_BLOCK_RANGE = "invalid block range params"; @@ -366,20 +371,6 @@ public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransac return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } - private byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { - if (!Pattern.matches(HASH_REGEX, hash)) { - throw new JsonRpcInvalidParamsException("invalid hash value"); - } - - byte[] bHash; - try { - bHash = ByteArray.fromHexString(hash); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(e.getMessage()); - } - return bHash; - } - /** * Reject any block selector that is not "latest". * Accepts "latest" silently; throws for other tags, numeric blocks, or invalid input. @@ -612,8 +603,19 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT throws JsonRpcInvalidParamsException { requireLatestBlockTag(blockNumOrTag); + if (storageIdx == null || storageIdx.length() > MAX_STORAGE_KEY_HEX_LEN) { + throw new JsonRpcInvalidParamsException("invalid storage key value"); + } + byte[] addressByte = addressCompatibleToByteArray(address); + DataWord index; + try { + index = new DataWord(ByteArray.fromHexString(storageIdx)); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid storage key value"); + } + // get contract from contractStore BytesMessage.Builder build = BytesMessage.newBuilder(); BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); @@ -627,7 +629,7 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT storage.setContractVersion(smartContract.getVersion()); storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); - DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); + DataWord value = storage.getValue(index); return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 0331ab3694a..3f9582d4825 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -6,7 +6,6 @@ import com.google.protobuf.ByteString; import lombok.Getter; import org.apache.commons.lang3.StringUtils; -import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; @@ -35,14 +34,14 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet long fromBlockSrc; long toBlockSrc; if (fr.getBlockHash() != null) { - String blockHash = ByteArray.fromHex(fr.getBlockHash()); if (fr.getFromBlock() != null || fr.getToBlock() != null) { throw new JsonRpcInvalidParamsException( "cannot specify both BlockHash and FromBlock/ToBlock, choose one or the other"); } + byte[] blockHashBytes = JsonRpcApiUtil.hashToByteArray(fr.getBlockHash()); Block block = null; if (wallet != null) { - block = wallet.getBlockById(ByteString.copyFrom(ByteArray.fromHexString(blockHash))); + block = wallet.getBlockById(ByteString.copyFrom(blockHashBytes)); } if (block == null) { throw new JsonRpcInvalidParamsException("invalid blockHash"); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index f753045d259..3de5d223b92 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -514,11 +514,9 @@ public void testBlockTagParsing() { () -> parseBlockNumber("abc", wallet)); Assert.assertEquals("Incorrect hex syntax", abcEx.getMessage()); - // parseBlockNumber: malformed hex -> throws Exception hexEx = Assert.assertThrows(Exception.class, () -> parseBlockNumber("0xxabc", wallet)); - // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed - Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); + Assert.assertEquals("invalid block number", hexEx.getMessage()); } @Test @@ -579,10 +577,29 @@ public void testGetStorageAt() { () -> tronJsonRpc.getStorageAt("", "", "abc")); Assert.assertEquals("invalid block number", e6.getMessage()); + // storageIdx length oversized -> invalid storage key value + String addr = "0xabd4b9367799eaa3197fecb144eb71de1e049abc"; + Exception e7 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, + "0x" + new String(new char[65]).replace('\0', 'a'), "latest")); + Assert.assertEquals("invalid storage key value", e7.getMessage()); + + // storageIdx is null -> invalid storage key value + Exception e8 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, null, "latest")); + Assert.assertEquals("invalid storage key value", e8.getMessage()); + + // storageIdx is valid length but decodes to >32 bytes (66 hex chars without 0x = 33 bytes): + // DataWord rejects it -> invalid storage key value (-32602), not a leaked Internal error. + Exception e9 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, + new String(new char[66]).replace('\0', 'a'), "latest")); + Assert.assertEquals("invalid storage key value", e9.getMessage()); + // latest happy path: address is an account, not a contract, so returns 32 zero bytes try { String value = tronJsonRpc.getStorageAt( - "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "0x0", "latest"); + addr, "0x0", "latest"); Assert.assertEquals(ByteArray.toJsonHex(new byte[32]), value); } catch (Exception e) { Assert.fail(); @@ -1006,6 +1023,29 @@ public void testLogFilterWrapper() { Assert.fail(); } + JsonRpcInvalidParamsException shortHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, "0x111111"), + 100, null, false)); + Assert.assertEquals("invalid hash value", shortHashEx.getMessage()); + + JsonRpcInvalidParamsException oversizedHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, + "0x" + new String(new char[1000]).replace('\0', 'a')), + 100, null, false)); + Assert.assertEquals("invalid hash value", oversizedHashEx.getMessage()); + + JsonRpcInvalidParamsException validHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, + "0x" + new String(new char[64]).replace('\0', 'a')), + 100, null, false)); + Assert.assertEquals("invalid blockHash", validHashEx.getMessage()); + // reset Args.getInstance().setJsonRpcMaxBlockRange(oldMaxBlockRange); } diff --git a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java index 466917d0cd5..31cfc174a77 100644 --- a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java @@ -14,6 +14,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.tron.common.crypto.ECKey; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.StringUtil; import org.tron.protos.Protocol.Account; public class GetAccountServletTest extends BaseHttpTest { @@ -57,4 +58,35 @@ public void testGetAccountGet() throws Exception { assertFalse("Should not contain error", content.contains("\"Error\"")); assertTrue("Should contain address", content.contains("address")); } + + @Test + public void testGetAccountPostOversizedBase58Address() throws Exception { + String oversized = addrStr + "0"; + String jsonParam = "{\"address\": \"" + oversized + "\", \"visible\": true}"; + MockHttpServletRequest request = postRequest(jsonParam); + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + String content = response.getContentAsString(); + // null from decodeFromBase58Check now surfaces a clear "invalid address" error instead of a + // bare NullPointerException (JsonFormat checks for null before ByteString.copyFrom). + assertTrue("oversized address should surface a clear invalid-address error: " + content, + content.contains("invalid address")); + } + + @Test + public void testGetAccountPostVisibleBase58Address() throws Exception { + // visible=true happy path: a valid 34-char Base58 address decodes back to the same 21 bytes + // (decodeFromBase58Check success path) and returns the account with no error. + String base58Addr = StringUtil.encode58Check(address); + String jsonParam = "{\"address\": \"" + base58Addr + "\", \"visible\": true}"; + MockHttpServletRequest request = postRequest(jsonParam); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).getAccount(argThat(req -> req != null && req.getAddress().equals(addr))); + String content = response.getContentAsString(); + assertFalse("Should not contain error", content.contains("\"Error\"")); + assertTrue("Should contain address", content.contains("address")); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java b/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java index 76f85da5d8f..9afa5607a66 100644 --- a/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java @@ -130,4 +130,35 @@ public void getByBlankParamTest() { } } + @Test + public void getRewardByOversizedValidCharAddressTest() { + // 41-char, all-valid-Base58 address: the length guard returns null -> reward 0. + MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("address", "T" + new String(new char[40]).replace('\0', 'a')); + new GetRewardServlet().doPost(request, response); + try { + JSONObject result = JSONObject.parseObject(response.getContentAsString()); + Assert.assertEquals(0, (int) result.get("reward")); + Assert.assertNull(result.get("Error")); + } catch (UnsupportedEncodingException e) { + Assert.fail(e.getMessage()); + } + } + + @Test + public void getRewardByOversizedIllegalCharAddressTest() { + MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("address", "T" + new String(new char[40]).replace('\0', '0')); + new GetRewardServlet().doPost(request, response); + try { + JSONObject result = JSONObject.parseObject(response.getContentAsString()); + Assert.assertEquals(0, (int) result.get("reward")); + Assert.assertNull(result.get("Error")); + } catch (UnsupportedEncodingException e) { + Assert.fail(e.getMessage()); + } + } + } 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 6aaeea2cc4e..be47f78339f 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 @@ -49,8 +49,8 @@ public void parseBlockNumberRejectsOverflow() { @Test public void parseBlockNumberRejectsOversized() { - // 101 chars exceeds the 100-char limit - String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + // 21 chars exceeds the 20-char limit + String tooLong = "0x" + new String(new char[18]).replace('\0', '0') + "1"; JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); assertEquals("invalid block number", e.getMessage()); @@ -75,4 +75,27 @@ public void parseBlockNumberRejectsEmpty() { assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.parseBlockNumber("")); } + + @Test + public void addressCompatibleToByteArrayNormal() + throws JsonRpcInvalidParamsException { + String addr = "0xabd4b9367799eaa3197fecb144eb71de1e049abc"; + assertEquals(21, JsonRpcApiUtil.addressCompatibleToByteArray(addr).length); + } + + @Test + public void addressCompatibleToByteArrayRejectsOversized() { + // 45 chars + String justOver = "0x" + new String(new char[43]).replace('\0', 'a'); + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.addressCompatibleToByteArray(justOver)); + assertEquals("invalid address hash value", e1.getMessage()); + } + + @Test + public void addressCompatibleToByteArrayRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.addressCompatibleToByteArray(null)); + assertEquals("invalid address hash value", e.getMessage()); + } } From 80c2d8ba0d99b9cc25d838c2da69a1f4deb4a970 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Tue, 9 Jun 2026 11:20:50 +0800 Subject: [PATCH 2/7] fix(jsonrpc): restrict HASH_REGEX to hex digits --- .../tron/core/services/jsonrpc/JsonRpcApiUtil.java | 2 +- .../core/services/jsonrpc/JsonRpcApiUtilTest.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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 bf7fc18119c..9849698850a 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 @@ -422,7 +422,7 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) } /** Matches a 32-byte hash hex string: optional 0x prefix + 64 hex chars (also caps length). */ - public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; + public static final String HASH_REGEX = "(0x)?[0-9a-fA-F]{64}$"; /** * Convert a hash hex string (optional 0x prefix) to a byte array, validating 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 be47f78339f..6dfe73c8aa1 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 @@ -98,4 +98,18 @@ public void addressCompatibleToByteArrayRejectsNull() { () -> JsonRpcApiUtil.addressCompatibleToByteArray(null)); assertEquals("invalid address hash value", e.getMessage()); } + + @Test + public void hashToByteArrayAcceptsValidHex() throws JsonRpcInvalidParamsException { + String hash = "0x" + new String(new char[64]).replace('\0', 'a'); + assertEquals(32, JsonRpcApiUtil.hashToByteArray(hash).length); + } + + @Test + public void hashToByteArrayRejectsNonHexChars() { + String hash = "0x" + new String(new char[64]).replace('\0', 'g'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.hashToByteArray(hash)); + assertEquals("invalid hash value", e.getMessage()); + } } From 1cf8948f75bf9dd3784e778cd61dd50f9c5daf37 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 10 Jun 2026 16:08:56 +0800 Subject: [PATCH 3/7] fix(jsonrpc): guard tx-index, ABI depth, fee-limit, quantity, topic and address Continue hardening the eth_* JSON-RPC parameter paths so malformed, oversized or overflowing inputs are rejected with a clear -32602 (or, for an out-of-range index, a null result) instead of leaking an Internal error, an uncaught Error/NPE, a DecoderException, or a silently wrong value. - eth_getTransactionByBlock{Hash,Number}AndIndex: parse the index via a new length-capped JsonRpcApiUtil.parseTxIndex (malformed / oversized / int-overflow index -> -32602); in the handler, a negative or out-of-range index (txIndex < 0 || >= tx count) returns a null result instead of letting block.getTransactions(-1) throw IndexOutOfBoundsException (-32603). - buildCreateSmartContractTransaction: a deeply nested ABI overflows the recursive JsonFormat parser; catch the StackOverflowError narrowly around the merge and map it to invalid-params ("invalid abi") instead of letting an Error escape. - buildCreate/buildTriggerSmartContractTransaction: compute feeLimit via JsonRpcApiUtil.calcFeeLimit (Math.multiplyExact) so an oversized gas no longer silently wraps gas*energyFee to a bogus (possibly negative) feeLimit. - parseQuantityValue: reject a signed ("0x-..") value/gas; QUANTITY is unsigned. - LogFilter topics: parse via the strict hashToByteArray (regex-validated 32-byte hash) instead of topicToByteArray, and remove the now-unused topicToByteArray. - addressToByteArray: cap length before fromHexString and wrap the decode so an oversized or invalid-hex address filter is invalid-params. --- .../java/org/tron/common/utils/Commons.java | 3 +- .../tron/core/services/http/JsonFormat.java | 5 +- .../core/services/jsonrpc/JsonRpcApiUtil.java | 70 +++++++++++++----- .../services/jsonrpc/TronJsonRpcImpl.java | 21 +++--- .../services/jsonrpc/filters/LogFilter.java | 6 +- .../org/tron/core/jsonrpc/JsonRpcTest.java | 20 ++++- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 50 +++++++++++++ .../services/jsonrpc/BuildArgumentsTest.java | 10 +++ .../services/jsonrpc/JsonRpcApiUtilTest.java | 73 ++++++++++++++++++- 9 files changed, 219 insertions(+), 39 deletions(-) diff --git a/chainbase/src/main/java/org/tron/common/utils/Commons.java b/chainbase/src/main/java/org/tron/common/utils/Commons.java index 634e2b34384..4ac04f9f4f9 100644 --- a/chainbase/src/main/java/org/tron/common/utils/Commons.java +++ b/chainbase/src/main/java/org/tron/common/utils/Commons.java @@ -52,8 +52,7 @@ public static byte[] decodeFromBase58Check(String addressBase58) { return null; } if (addressBase58.length() != BASE58_ADDRESS_LENGTH) { - logger.warn("Warning: invalid Base58 address length: {} ,expected {} !!", - addressBase58.length(), BASE58_ADDRESS_LENGTH); + logger.warn("Warning: invalid Base58 address length !!"); return null; } byte[] address = decode58Check(addressBase58); diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index e0f00181817..70b59e72b4d 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -1313,12 +1313,11 @@ static ByteString unescapeBytesSelfType(String input, final String fliedName) throws InvalidEscapeSequence { //Address base58 -> ByteString if (HttpSelfFormatFieldName.isAddressFormat(fliedName)) { - byte[] addressBytes; + byte[] addressBytes = null; try { addressBytes = Commons.decodeFromBase58Check(input); } catch (IllegalArgumentException e) { - // Base58.decode throw - addressBytes = null; + // Base58.decode throws on illegal chars -> leave addressBytes null (treated as invalid) } if (addressBytes == null) { // empty / wrong-length / bad-checksum / illegal chars -> all invalid addresses; throw a 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 9849698850a..9ae3d58a413 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 @@ -61,6 +61,7 @@ public class JsonRpcApiUtil { public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; public static final String BLOCK_NUM_ERROR = "invalid block number"; + public static final String TX_INDEX_ERROR = "invalid index value"; private static final SecureRandom random = new SecureRandom(); @@ -399,21 +400,21 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) // ADDRESS_SIZE (42) is the hex length of a 21-byte address; +2 leaves room for the optional // "0x" prefix, so a 0x-prefixed 21-byte address (0x41..., 44 chars) is still accepted. if (hexAddress == null || hexAddress.length() > DecodeUtil.ADDRESS_SIZE + 2) { - throw new JsonRpcInvalidParamsException("invalid address hash value"); + throw new JsonRpcInvalidParamsException("invalid address"); } byte[] addressByte; try { addressByte = ByteArray.fromHexString(hexAddress); if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 && addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { - throw new JsonRpcInvalidParamsException("invalid address hash value"); + throw new JsonRpcInvalidParamsException("invalid address"); } if (addressByte.length == DecodeUtil.ADDRESS_SIZE / 2 - 1) { addressByte = ByteUtil.merge(new byte[] {DecodeUtil.addressPreFixByte}, addressByte); } else if (addressByte[0] != ByteArray.fromHexString(DecodeUtil.addressPreFixString)[0]) { // addressByte.length == DecodeUtil.ADDRESS_SIZE / 2 - throw new JsonRpcInvalidParamsException("invalid address hash value"); + throw new JsonRpcInvalidParamsException("invalid address"); } } catch (Exception e) { throw new JsonRpcInvalidParamsException(e.getMessage()); @@ -429,7 +430,7 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) * format and length via {@link #HASH_REGEX} first. */ public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { - if (!Pattern.matches(HASH_REGEX, hash)) { + if (hash == null || !Pattern.matches(HASH_REGEX, hash)) { throw new JsonRpcInvalidParamsException("invalid hash value"); } byte[] bHash; @@ -445,24 +446,23 @@ public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsExc * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. */ public static byte[] addressToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { - byte[] addressByte = ByteArray.fromHexString(hexAddress); + if (hexAddress == null) { + throw new JsonRpcInvalidParamsException("address is null"); + } else if (hexAddress.length() > DecodeUtil.ADDRESS_SIZE) { + throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + } + byte[] addressByte; + try { + addressByte = ByteArray.fromHexString(hexAddress); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + } if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); } return new DataWord(addressByte).getLast20Bytes(); } - /** - * check if topic is hex string of size 64, padding 0 ahead if length is odd. - */ - public static byte[] topicToByteArray(String hexTopic) throws JsonRpcInvalidParamsException { - byte[] topicByte = ByteArray.fromHexString(hexTopic); - if (topicByte.length != 32) { - throw new JsonRpcInvalidParamsException("invalid topic: " + hexTopic); - } - return topicByte; - } - public static boolean paramStringIsNull(String string) { return StringUtils.isEmpty(string) || string.equals("0x"); } @@ -525,7 +525,10 @@ public static long parseQuantityValue(String value) throws JsonRpcInvalidParamsE throw new JsonRpcInvalidParamsException("invalid param value: invalid hex number"); } } - + // QUANTITY is unsigned; reject a signed ("0x-..") value instead of returning a negative. + if (callValue < 0) { + throw new JsonRpcInvalidParamsException("invalid param value: negative"); + } return callValue; } @@ -676,6 +679,39 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) return parseBlockNumber(blockNumOrTag); } + /** + * Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the + * longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix. + */ + private static final int MAX_TX_INDEX_HEX_LEN = 8; + + /** + * Parse a 0x-prefixed hex transaction index at the JSON-RPC boundary. + */ + public static int parseTxIndex(String index) throws JsonRpcInvalidParamsException { + if (index == null || index.length() > MAX_TX_INDEX_HEX_LEN + 2) { + throw new JsonRpcInvalidParamsException(TX_INDEX_ERROR); + } + try { + return ByteArray.jsonHexToInt(index); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(TX_INDEX_ERROR); + } + } + + /** + * Compute feeLimit = gas * energyFee with overflow protection. A gas value large enough to + * overflow a signed 64-bit feeLimit is rejected as invalid-params instead of silently wrapping + * to a bogus (possibly negative) value. + */ + public static long calcFeeLimit(long gas, long energyFee) throws JsonRpcInvalidParamsException { + try { + return Math.multiplyExact(gas, energyFee); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException("invalid gas: fee limit overflow"); + } + } + public static String generateFilterId() { byte[] uid = new byte[16]; // 128 bits are converted to 16 bytes random.nextBytes(uid); 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 f536a47c6fb..6ddf12454a7 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 @@ -7,12 +7,14 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.HASH_REGEX; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.calcFeeLimit; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.generateFilterId; 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.hashToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseTxIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.google.common.annotations.VisibleForTesting; @@ -814,14 +816,9 @@ private TransactionResult formatTransactionResult(TransactionInfo transactioninf private TransactionResult getTransactionByBlockAndIndex(Block block, String index) throws JsonRpcInvalidParamsException { - int txIndex; - try { - txIndex = ByteArray.jsonHexToInt(index); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid index value"); - } + int txIndex = parseTxIndex(index); - if (txIndex >= block.getTransactionsCount()) { + if (txIndex < 0 || txIndex >= block.getTransactionsCount()) { return null; } @@ -1143,7 +1140,11 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, ABI.Builder abiBuilder = ABI.newBuilder(); if (StringUtils.isNotEmpty(args.getAbi())) { String abiStr = "{" + "\"entrys\":" + args.getAbi() + "}"; - JsonFormat.merge(abiStr, abiBuilder, args.isVisible()); + try { + JsonFormat.merge(abiStr, abiBuilder, args.isVisible()); + } catch (StackOverflowError e) { + throw new JsonRpcInvalidParamsException("invalid abi"); + } } SmartContract.Builder smartBuilder = SmartContract.newBuilder(); @@ -1169,7 +1170,7 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, .createTransactionCapsule(build.build(), ContractType.CreateSmartContract).getInstance(); Transaction.Builder txBuilder = tx.toBuilder(); Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); - rawBuilder.setFeeLimit(args.parseGas() * wallet.getEnergyFee()); + rawBuilder.setFeeLimit(calcFeeLimit(args.parseGas(), wallet.getEnergyFee())); txBuilder.setRawData(rawBuilder); tx = setTransactionPermissionId(args.getPermissionId(), txBuilder.build()); @@ -1218,7 +1219,7 @@ private TransactionJson buildTriggerSmartContractTransaction(byte[] ownerAddress Transaction.Builder txBuilder = tx.toBuilder(); Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); - rawBuilder.setFeeLimit(args.parseGas() * wallet.getEnergyFee()); + rawBuilder.setFeeLimit(calcFeeLimit(args.parseGas(), wallet.getEnergyFee())); txBuilder.setRawData(rawBuilder); Transaction trx = wallet diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java index d2bd58f6c56..e5e16d73079 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java @@ -1,7 +1,7 @@ package org.tron.core.services.jsonrpc.filters; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressToByteArray; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.topicToByteArray; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.hashToByteArray; import com.google.protobuf.ByteString; import java.util.ArrayList; @@ -81,7 +81,7 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { withTopic((byte[][]) null); } else if (topic instanceof String) { try { - withTopic(new DataWord(topicToByteArray((String) topic)).getData()); + withTopic(new DataWord(hashToByteArray((String) topic)).getData()); } catch (JsonRpcInvalidParamsException e) { throw new JsonRpcInvalidParamsException("invalid topic(s): " + topic); } @@ -94,7 +94,7 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { List t = new ArrayList<>(); for (Object s : ((ArrayList) topic)) { try { - t.add(new DataWord(topicToByteArray((String) s)).getData()); + t.add(new DataWord(hashToByteArray((String) s)).getData()); } catch (JsonRpcInvalidParamsException e) { throw new JsonRpcInvalidParamsException("invalid topic(s): " + s); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 5f577194dff..d04ac391c8e 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -143,13 +143,13 @@ public void testAddressCompatibleToByteArray() { try { addressCompatibleToByteArray(rawAddress.substring(1)); } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("invalid address hash value", e.getMessage()); + Assert.assertEquals("invalid address", e.getMessage()); } try { addressCompatibleToByteArray(rawAddress + "00"); } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("invalid address hash value", e.getMessage()); + Assert.assertEquals("invalid address", e.getMessage()); } } @@ -177,6 +177,22 @@ public void testAddressToByteArray() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } + + // oversized input rejected before fromHexString + try { + addressToByteArray("0x" + new String(new char[64]).replace('\0', 'a')); + Assert.fail(); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("invalid address")); + } + + // invalid hex char -> invalid params, not a leaked DecoderException + try { + addressToByteArray("0x548794500882809695a8a687866e76d4271a1abz"); + Assert.fail(); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("invalid address")); + } } /** diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index 3de5d223b92..0a45a241d2e 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -56,6 +56,7 @@ import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; +import org.tron.core.services.jsonrpc.types.BuildArguments; import org.tron.core.services.jsonrpc.types.TransactionReceipt; import org.tron.core.services.jsonrpc.types.TransactionResult; import org.tron.json.JSON; @@ -691,6 +692,31 @@ public void testGetTransactionByBlockNumberAndIndex() { Assert.fail(); } + // negative index is out of range too -> null (not an Internal error) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x-1"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // leading zeros are tolerated: "0x00" parses to index 0 + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x00"); + Assert.assertNotNull(result); + Assert.assertEquals(ByteArray.toJsonHex(0L), result.getTransactionIndex()); + } catch (Exception e) { + Assert.fail(); + } + + // oversized index (> 8 hex digits) rejected before parsing + Exception oversizedEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x123456789")); + Assert.assertEquals("invalid index value", oversizedEx.getMessage()); + // latest -> blockCapsule1 (head) try { TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("latest", "0x0"); @@ -1429,6 +1455,30 @@ public void testBuildTransactionTransfer() { } } + @Test + public void testBuildTransactionRejectsDeeplyNestedAbi() { + // A pathological ABI with deep nesting overflows the recursive JsonFormat parser. + // buildCreateSmartContractTransaction must surface this as invalid-params (-32602), + // not let a StackOverflowError escape as a generic internal error. + int depth = 200_000; + StringBuilder abi = new StringBuilder("[],\"x\":"); + for (int i = 0; i < depth; i++) { + abi.append('['); + } + for (int i = 0; i < depth; i++) { + abi.append(']'); + } + + BuildArguments args = new BuildArguments(); + args.setFrom("0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.setData("60806040"); + args.setAbi(abi.toString()); + + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.buildTransaction(args)); + Assert.assertEquals("invalid abi", e.getMessage()); + } + @Test public void testWeb3ClientVersion() { try { diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 753d93d47f4..3f2a91bea1c 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -69,6 +69,16 @@ public void testParseGas() throws JsonRpcInvalidParamsException { Assert.assertEquals(16L, gas); } + @Test + public void parseValueRejectsNegative() { + // QUANTITY is unsigned; a signed "0x-.." value must be rejected, not returned as negative. + BuildArguments args = new BuildArguments(); + args.setValue("0x-1"); + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> args.parseValue()); + Assert.assertEquals("invalid param value: negative", e.getMessage()); + } + @Test public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { BuildArguments args = new BuildArguments(); 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 6dfe73c8aa1..0c2ea9f363c 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 @@ -89,14 +89,14 @@ public void addressCompatibleToByteArrayRejectsOversized() { String justOver = "0x" + new String(new char[43]).replace('\0', 'a'); JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.addressCompatibleToByteArray(justOver)); - assertEquals("invalid address hash value", e1.getMessage()); + assertEquals("invalid address", e1.getMessage()); } @Test public void addressCompatibleToByteArrayRejectsNull() { JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.addressCompatibleToByteArray(null)); - assertEquals("invalid address hash value", e.getMessage()); + assertEquals("invalid address", e.getMessage()); } @Test @@ -112,4 +112,73 @@ public void hashToByteArrayRejectsNonHexChars() { () -> JsonRpcApiUtil.hashToByteArray(hash)); assertEquals("invalid hash value", e.getMessage()); } + + @Test + public void parseTxIndexAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1a, JsonRpcApiUtil.parseTxIndex("0x1a")); + assertEquals(0, JsonRpcApiUtil.parseTxIndex("0x0")); + // 8 hex digits is the max width; 0x7fffffff is Integer.MAX_VALUE + assertEquals(Integer.MAX_VALUE, JsonRpcApiUtil.parseTxIndex("0x7fffffff")); + } + + @Test + public void parseTxIndexRejectsMissingPrefix() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("1a")); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexAcceptsLeadingZeros() throws JsonRpcInvalidParamsException { + // leading zeros are tolerated (only length is capped); "0x01"/"0x00" parse normally + assertEquals(1, JsonRpcApiUtil.parseTxIndex("0x01")); + assertEquals(0, JsonRpcApiUtil.parseTxIndex("0x00")); + } + + @Test + public void parseTxIndexRejectsOversized() { + // 9 hex digits exceeds the 8-digit (0x + 8) limit -> rejected before parsing + String tooLong = "0x" + new String(new char[9]).replace('\0', '1'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex(tooLong)); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexRejectsEmptyAndNull() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("0x")); + assertEquals("invalid index value", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex(null)); + assertEquals("invalid index value", e2.getMessage()); + } + + @Test + public void parseTxIndexRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("0xGG")); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexParsesNegativeForCallerRangeCheck() throws JsonRpcInvalidParamsException { + // "0x-1" is syntactically accepted and parsed to a negative int; the RPC handler maps + // any out-of-range index (negative or >= tx count) to a null result. + assertEquals(-1, JsonRpcApiUtil.parseTxIndex("0x-1")); + } + + @Test + public void calcFeeLimitNormal() throws JsonRpcInvalidParamsException { + assertEquals(8400L, JsonRpcApiUtil.calcFeeLimit(20L, 420L)); + assertEquals(0L, JsonRpcApiUtil.calcFeeLimit(0L, 420L)); + } + + @Test + public void calcFeeLimitRejectsOverflow() { + // gas * energyFee overflows int64 -> rejected instead of silently wrapping to a bogus feeLimit + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.calcFeeLimit(Long.MAX_VALUE, 420L)); + assertEquals("invalid gas: fee limit overflow", e.getMessage()); + } } From 53f03489fbf15f5a0083555b4d7db3866069a732 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 10 Jun 2026 16:23:35 +0800 Subject: [PATCH 4/7] fix(jsonrpc): use StrictMathWrapper.multiplyExact in calcFeeLimit --- .../java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9ae3d58a413..1cef19c37f7 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 @@ -14,6 +14,7 @@ import org.bouncycastle.util.encoders.Hex; import org.tron.api.GrpcAPI.AssetIssueList; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; @@ -706,7 +707,7 @@ public static int parseTxIndex(String index) throws JsonRpcInvalidParamsExceptio */ public static long calcFeeLimit(long gas, long energyFee) throws JsonRpcInvalidParamsException { try { - return Math.multiplyExact(gas, energyFee); + return StrictMathWrapper.multiplyExact(gas, energyFee); } catch (ArithmeticException e) { throw new JsonRpcInvalidParamsException("invalid gas: fee limit overflow"); } From 4f36b67fd68f9fdcd997c9c7c9c70d778f396f8d Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 10 Jun 2026 18:58:18 +0800 Subject: [PATCH 5/7] fix(jsonrpc): guard null block params (getBlockReceipts, parseBlockNumber) --- .../java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java | 2 +- .../java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java | 3 ++- .../test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) 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 1cef19c37f7..0b6d1528215 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 @@ -674,7 +674,7 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) if (isBlockTag(blockNumOrTag)) { return parseBlockTag(blockNumOrTag, wallet); } - if (!blockNumOrTag.startsWith("0x")) { + if (blockNumOrTag == null || !blockNumOrTag.startsWith("0x")) { throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); } return parseBlockNumber(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 6ddf12454a7..82568755892 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 @@ -933,9 +933,10 @@ public List getBlockReceipts(String blockNumOrHashOrTag) Block block = null; - if (Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { + if (blockNumOrHashOrTag != null && Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { block = getBlockByJsonHash(blockNumOrHashOrTag); } else { + // null falls through to getBlockByNumOrTag -> parseBlockNumber -> -32602 (not an NPE) block = getBlockByNumOrTag(blockNumOrHashOrTag); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index 0a45a241d2e..870ce317663 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1378,6 +1378,10 @@ public void testGetBlockReceipts() { () -> tronJsonRpc.getBlockReceipts("test")); Assert.assertEquals("invalid block number", testReceiptsEx.getMessage()); + Exception nullReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts(null)); + Assert.assertEquals("invalid block number", nullReceiptsEx.getMessage()); + try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); Assert.assertNull(transactionReceiptList); From 40cb2739e564c727f6c6cfb0d322587d288342a0 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 11 Jun 2026 00:03:07 +0800 Subject: [PATCH 6/7] fix(jsonrpc): restore odd-length topic compat, reject non-string filter elements - Restore topicToByteArray for LogFilter topics, guard with (0x)?[0-9a-fA-F]{63,64}$ so the stripped zero is padded back by ByteArray.fromHexString, while non-hex or wrong-length input still gets a clean -32602. - LogFilter: validate element types before the (String) cast in the address array and nested topic array loops. --- .../core/services/jsonrpc/JsonRpcApiUtil.java | 20 +++++++++++ .../services/jsonrpc/filters/LogFilter.java | 13 +++++-- .../org/tron/core/jsonrpc/JsonRpcTest.java | 29 ++++++++++++++- .../services/jsonrpc/JsonRpcApiUtilTest.java | 35 +++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) 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 0b6d1528215..9dabcceb90a 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 @@ -443,6 +443,26 @@ public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsExc return bHash; } + /** + * Matches a 32-byte topic hex string: optional 0x prefix + 63 or 64 hex chars. + */ + public static final String TOPIC_REGEX = "(0x)?[0-9a-fA-F]{63,64}$"; + + /** + * Convert a topic hex string (optional 0x prefix, leading zero may be omitted) to a 32-byte + * array, validating format and length via {@link #TOPIC_REGEX} first. + */ + public static byte[] topicToByteArray(String hexTopic) throws JsonRpcInvalidParamsException { + if (hexTopic == null || !Pattern.matches(TOPIC_REGEX, hexTopic)) { + throw new JsonRpcInvalidParamsException("invalid topic: " + hexTopic); + } + try { + return ByteArray.fromHexString(hexTopic); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid topic: " + hexTopic); + } + } + /** * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. */ diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java index e5e16d73079..03232f3549d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java @@ -1,7 +1,7 @@ package org.tron.core.services.jsonrpc.filters; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressToByteArray; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.hashToByteArray; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.topicToByteArray; import com.google.protobuf.ByteString; import java.util.ArrayList; @@ -57,6 +57,10 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { List addr = new ArrayList<>(); int i = 0; for (Object s : (ArrayList) fr.getAddress()) { + if (!(s instanceof String)) { + throw new JsonRpcInvalidParamsException( + String.format("invalid address at index %d: %s", i, s)); + } try { addr.add(addressToByteArray((String) s)); i++; @@ -81,7 +85,7 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { withTopic((byte[][]) null); } else if (topic instanceof String) { try { - withTopic(new DataWord(hashToByteArray((String) topic)).getData()); + withTopic(new DataWord(topicToByteArray((String) topic)).getData()); } catch (JsonRpcInvalidParamsException e) { throw new JsonRpcInvalidParamsException("invalid topic(s): " + topic); } @@ -93,8 +97,11 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { List t = new ArrayList<>(); for (Object s : ((ArrayList) topic)) { + if (!(s instanceof String)) { + throw new JsonRpcInvalidParamsException("invalid topic(s): " + s); + } try { - t.add(new DataWord(hashToByteArray((String) s)).getData()); + t.add(new DataWord(topicToByteArray((String) s)).getData()); } catch (JsonRpcInvalidParamsException e) { throw new JsonRpcInvalidParamsException("invalid topic(s): " + s); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index d04ac391c8e..49f875f3823 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -201,7 +201,7 @@ public void testAddressToByteArray() { @Test public void testLogFilter() { - //topic must be 64 hex string + //topic must be 63 or 64 hex string, full 64-char form here try { new LogFilter(new FilterRequest(null, null, null, new String[] {"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"}, @@ -210,6 +210,19 @@ public void testLogFilter() { Assert.fail(); } + //63-char form: leading zero stripped by some clients, padded back to the same topic + String paddedAddressTopic = + "000000000000000000000000f0cc5a2a84cd0f68ed1667070934542d673acbd8"; + try { + LogFilter full = new LogFilter(new FilterRequest(null, null, null, + new String[] {null, "0x" + paddedAddressTopic}, null)); + LogFilter stripped = new LogFilter(new FilterRequest(null, null, null, + new String[] {null, "0x" + paddedAddressTopic.substring(1)}, null)); + Assert.assertArrayEquals(full.getTopics().get(1)[0], stripped.getTopics().get(1)[0]); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail(); + } + try { new LogFilter(new FilterRequest(null, null, null, new String[] {"0x0"}, null)); } catch (JsonRpcInvalidParamsException e) { @@ -225,6 +238,20 @@ public void testLogFilter() { Assert.assertTrue(e.getMessage().contains("invalid topic")); } + // non-string element in address array -> -32602, not a leaked ClassCastException + JsonRpcInvalidParamsException badAddrElement = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilter(new FilterRequest(null, null, + new ArrayList<>(Collections.singletonList(1)), null, null))); + Assert.assertEquals("invalid address at index 0: 1", badAddrElement.getMessage()); + + // non-string element in nested topic array -> -32602, not a leaked ClassCastException + JsonRpcInvalidParamsException badTopicElement = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilter(new FilterRequest(null, null, null, + new Object[] {new ArrayList<>(Collections.singletonList(1))}, null))); + Assert.assertEquals("invalid topic(s): 1", badTopicElement.getMessage()); + // topic size should be <= 4 try { new LogFilter(new FilterRequest(null, null, null, 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 0c2ea9f363c..f719446df72 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 @@ -113,6 +113,41 @@ public void hashToByteArrayRejectsNonHexChars() { assertEquals("invalid hash value", e.getMessage()); } + @Test + public void topicToByteArrayAcceptsFullLengthHex() throws JsonRpcInvalidParamsException { + String topic = "0x" + new String(new char[64]).replace('\0', 'a'); + assertEquals(32, JsonRpcApiUtil.topicToByteArray(topic).length); + } + + @Test + public void topicToByteArrayPadsMissingLeadingZero() throws JsonRpcInvalidParamsException { + String stripped = "0x" + new String(new char[63]).replace('\0', 'a'); + byte[] parsed = JsonRpcApiUtil.topicToByteArray(stripped); + assertEquals(32, parsed.length); + assertEquals(0x0a, parsed[0]); + } + + @Test + public void topicToByteArrayRejectsNonHexChars() { + String topic = "0x" + new String(new char[64]).replace('\0', 'g'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(topic)); + assertEquals("invalid topic: " + topic, e.getMessage()); + } + + @Test + public void topicToByteArrayRejectsWrongLength() { + // 62 chars (two zeros stripped) and 65 chars are both invalid + String tooShort = "0x" + new String(new char[62]).replace('\0', 'a'); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(tooShort)); + String tooLong = "0x" + new String(new char[65]).replace('\0', 'a'); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(tooLong)); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(null)); + } + @Test public void parseTxIndexAcceptsHex() throws JsonRpcInvalidParamsException { assertEquals(0x1a, JsonRpcApiUtil.parseTxIndex("0x1a")); From e5a033ab215ad59a1c343d95337aedac4007fb38 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 11 Jun 2026 15:24:49 +0800 Subject: [PATCH 7/7] fix(jsonrpc): quiet address-funnel log, anchor hash/topic regexes --- chainbase/src/main/java/org/tron/common/utils/Commons.java | 4 ++-- .../java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chainbase/src/main/java/org/tron/common/utils/Commons.java b/chainbase/src/main/java/org/tron/common/utils/Commons.java index 4ac04f9f4f9..99c20d67f11 100644 --- a/chainbase/src/main/java/org/tron/common/utils/Commons.java +++ b/chainbase/src/main/java/org/tron/common/utils/Commons.java @@ -48,11 +48,11 @@ public static byte[] decode58Check(String input) { */ public static byte[] decodeFromBase58Check(String addressBase58) { if (StringUtils.isEmpty(addressBase58)) { - logger.warn("Warning: Address is empty !!"); + logger.debug("address is empty !!"); return null; } if (addressBase58.length() != BASE58_ADDRESS_LENGTH) { - logger.warn("Warning: invalid Base58 address length !!"); + logger.debug("invalid Base58 address length"); return null; } byte[] address = decode58Check(addressBase58); 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 9dabcceb90a..f4bba9fbf37 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 @@ -424,7 +424,7 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) } /** Matches a 32-byte hash hex string: optional 0x prefix + 64 hex chars (also caps length). */ - public static final String HASH_REGEX = "(0x)?[0-9a-fA-F]{64}$"; + public static final String HASH_REGEX = "^(0x)?[0-9a-fA-F]{64}$"; /** * Convert a hash hex string (optional 0x prefix) to a byte array, validating @@ -446,7 +446,7 @@ public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsExc /** * Matches a 32-byte topic hex string: optional 0x prefix + 63 or 64 hex chars. */ - public static final String TOPIC_REGEX = "(0x)?[0-9a-fA-F]{63,64}$"; + public static final String TOPIC_REGEX = "^(0x)?[0-9a-fA-F]{63,64}$"; /** * Convert a topic hex string (optional 0x prefix, leading zero may be omitted) to a 32-byte