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..99c20d67f11 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,9 +43,16 @@ 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 !!"); + logger.debug("address is empty !!"); + return null; + } + if (addressBase58.length() != 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/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 1dab6c7b941..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,7 +1313,18 @@ 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 = null; + try { + addressBytes = Commons.decodeFromBase58Check(input); + } catch (IllegalArgumentException e) { + // 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 + // 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..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 @@ -8,11 +8,13 @@ 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; 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; @@ -60,6 +62,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(); @@ -395,19 +398,24 @@ 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"); + } 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()); @@ -415,26 +423,65 @@ 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)?[0-9a-fA-F]{64}$"; + /** - * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. + * Convert a hash hex string (optional 0x prefix) to a byte array, validating + * format and length via {@link #HASH_REGEX} first. */ - public static byte[] addressToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { - byte[] addressByte = ByteArray.fromHexString(hexAddress); - if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { - throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { + if (hash == null || !Pattern.matches(HASH_REGEX, hash)) { + throw new JsonRpcInvalidParamsException("invalid hash value"); } - return new DataWord(addressByte).getLast20Bytes(); + byte[] bHash; + try { + bHash = ByteArray.fromHexString(hash); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(e.getMessage()); + } + return bHash; } /** - * check if topic is hex string of size 64, padding 0 ahead if length is odd. + * 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 { - byte[] topicByte = ByteArray.fromHexString(hexTopic); - if (topicByte.length != 32) { + 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); } - return topicByte; + } + + /** + * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. + */ + public static byte[] addressToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { + 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(); } public static boolean paramStringIsNull(String string) { @@ -499,7 +546,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; } @@ -603,10 +653,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 +687,50 @@ 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 == null || !blockNumOrTag.startsWith("0x")) { + throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); + } + 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 StrictMathWrapper.multiplyExact(gas, energyFee); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException("invalid gas: fee limit overflow"); + } } 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..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 @@ -3,15 +3,18 @@ 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.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; @@ -157,7 +160,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 +373,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 +605,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 +631,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()); } @@ -812,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; } @@ -934,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); } @@ -1141,7 +1141,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(); @@ -1167,7 +1171,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()); @@ -1216,7 +1220,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..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 @@ -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++; @@ -93,6 +97,9 @@ 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(topicToByteArray((String) s)).getData()); } catch (JsonRpcInvalidParamsException e) { 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/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 5f577194dff..49f875f3823 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")); + } } /** @@ -185,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"}, @@ -194,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) { @@ -209,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/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index f753045d259..870ce317663 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; @@ -514,11 +515,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 +578,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(); @@ -674,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"); @@ -1006,6 +1049,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); } @@ -1312,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); @@ -1389,6 +1459,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/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/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 6aaeea2cc4e..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 @@ -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,145 @@ 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", e1.getMessage()); + } + + @Test + public void addressCompatibleToByteArrayRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.addressCompatibleToByteArray(null)); + assertEquals("invalid address", 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()); + } + + @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")); + 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()); + } }