diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..d9f3ee7406b 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -31,6 +31,7 @@ import static org.tron.core.config.Parameter.DatabaseConstants.MARKET_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.PROPOSAL_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.WITNESS_COUNT_LIMIT_MAX; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseEnergyFee; import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; @@ -247,7 +248,6 @@ import org.tron.protos.contract.BalanceContract.BlockBalanceTrace; import org.tron.protos.contract.BalanceContract.TransferContract; import org.tron.protos.contract.Common; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; import org.tron.protos.contract.ShieldContract.OutputPoint; import org.tron.protos.contract.ShieldContract.OutputPointInfo; @@ -575,41 +575,41 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(true).setCode(response_code.SUCCESS).build(); } } catch (ValidateSignatureException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.SIGERROR) .setMessage(ByteString.copyFromUtf8("Validate signature error: " + e.getMessage())) .build(); } catch (ContractValidateException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) .setMessage(ByteString.copyFromUtf8(CONTRACT_VALIDATE_ERROR + e.getMessage())) .build(); } catch (ContractExeException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) .setMessage(ByteString.copyFromUtf8("Contract execute error : " + e.getMessage())) .build(); } catch (AccountResourceInsufficientException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.BANDWITH_ERROR) .setMessage(ByteString.copyFromUtf8("Account resource insufficient error.")) .build(); } catch (DupTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8("Dup transaction.")) .build(); } catch (TaposException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TAPOS_ERROR) .setMessage(ByteString.copyFromUtf8("Tapos check error.")) .build(); } catch (TooBigTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); } catch (TransactionExpirationException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); @@ -743,13 +743,7 @@ public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { } else if (PENDING_STR.equalsIgnoreCase(id)) { throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); } else { - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(id).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid block number"); - } - + long blockNumber = parseBlockNumber(id); return getBlockByNum(blockNumber); } } @@ -2207,23 +2201,6 @@ public IncrementalMerkleVoucherInfo getMerkleTreeVoucherInfo(OutputPointInfo req return result.build(); } - public IncrementalMerkleTree getMerkleTreeOfBlock(long blockNum) throws ZksnarkException { - checkAllowShieldedTransactionApi(); - if (blockNum < 0) { - return null; - } - - try { - if (chainBaseManager.getMerkleTreeIndexStore().has(ByteArray.fromLong(blockNum))) { - return IncrementalMerkleTree - .parseFrom(chainBaseManager.getMerkleTreeIndexStore().get(blockNum)); - } - } catch (Exception ex) { - logger.error("GetMerkleTreeOfBlock failed, blockNum:{}", blockNum, ex); - } - - return null; - } public long getShieldedTransactionFee() { return chainBaseManager.getDynamicPropertiesStore().getShieldedTransactionFee(); @@ -2958,13 +2935,6 @@ public MarketOrderList getMarketOrderListByPair(byte[] sellTokenId, byte[] buyTo return builder.build(); } - public Transaction deployContract(TransactionCapsule trxCap) { - - // do nothing, so can add some useful function later - // trxCap contract para cacheUnpackValue has value - - return trxCap.getInstance(); - } public Transaction triggerContract(TriggerSmartContract triggerSmartContract, diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 63e7ba03fc7..bc50b79a36f 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -418,7 +418,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.error("Solidity NonUniqueObjectException: {}", e.getMessage()); + logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..1fadc7a6e54 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,7 +3,11 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,56 +35,66 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + static final Map> ALLOWED_ADAPTERS; + static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + List> adapters = Arrays.asList( + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class); + Map> m = new HashMap<>(); + for (Class c : adapters) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @PostConstruct private void addRateContainer() { - RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() - .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } + RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() + .getRateLimiterInitialization().getHttpMap().get(name); + + String cName; + String params; + if (item == null) { + cName = DEFAULT_ADAPTER_NAME; + params = QpsStrategy.DEFAULT_QPS_PARAM; + } else { + cName = item.getStrategy(); + params = item.getParams(); } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); - } + + try { + container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); + } catch (Exception e) { + throw throwTronError(cName, params, name, e); + } + } + + static IRateLimiter buildAdapter(String cName, String params, String name) { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw throwTronError(cName, params, name, + new IllegalArgumentException("unknown rate limiter adapter; allowed=" + + ALLOWED_ADAPTERS.keySet())); + } + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + throw throwTronError(cName, params, name, e); } } - private void throwTronError(String strategy, String params, String servlet, Exception e) { - throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + private static TronError throwTronError(String strategy, String params, String servlet, + Exception e) { + return new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", e, TronError.ErrCode.RATE_LIMITER_INIT); } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..a0e8c77d646 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -95,7 +95,7 @@ public static String printErrorMsg(Exception e) { public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = new JSONObject(); JSONArray jsonArray = new JSONArray(); blocks.stream().forEach(block -> jsonArray.add(printBlockToJSON(block, selfType))); jsonObject.put("block", jsonArray); @@ -110,8 +110,10 @@ public static String printBlock(Block block, boolean selfType) { public static JSONObject printBlockToJSON(Block block, boolean selfType) { BlockCapsule blockCapsule = new BlockCapsule(block); String blockID = ByteArray.toHexString(blockCapsule.getBlockId().getBytes()); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(block, selfType)); + JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); + jsonObject.put("block_header", + JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 4a60f14b534..c3aca97e945 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -10,6 +10,7 @@ import com.google.common.primitives.Longs; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; @@ -515,6 +516,43 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { return -1; } + /** + * Max allowed length for a JSON-RPC block number hex/decimal input. + * API-level DoS guard: rejects pathological inputs before BigInteger parsing, + * whose cost grows quadratically with length. Covers hex (0x + 64 chars for + * uint256) and decimal (78 chars for uint256) representations with headroom. + */ + private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + + private static final String BLOCK_NUM_ERROR = "invalid block number"; + + /** + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit, rejecting + * negative values, and rejecting values that overflow a signed 64-bit + * block number. + */ + public static long parseBlockNumber(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (blockNumOrTag == null || blockNumOrTag.length() > MAX_BLOCK_NUM_HEX_LEN) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + BigInteger value; + try { + value = ByteArray.hexToBigInteger(blockNumOrTag); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + if (value.signum() < 0) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return value.longValueExact(); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + } + public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) throws JsonRpcInvalidParamsException { if (PENDING_STR.equalsIgnoreCase(blockNumOrTag)) { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index de939bdfff4..dd570dbd487 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -8,6 +8,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.alibaba.fastjson.JSON; @@ -409,12 +410,7 @@ public String getTrxBalance(String address, String blockNumOrTag) } return ByteArray.toJsonHex(balance); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -558,12 +554,7 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -589,12 +580,7 @@ public String getABIOfSmartContract(String contractAddress, String blockNumOrTag } } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } @@ -971,12 +957,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); @@ -1014,12 +995,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) return call(addressData, contractAddressData, transactionCall.parseValue(), ByteArray.fromHexString(transactionCall.getData())); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } } diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java new file mode 100644 index 00000000000..4ae76f85dfb --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -0,0 +1,93 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies RateLimiterServlet's adapter resolution: strict whitelist + * (no Class.forName arbitrary class loading), fail-fast on unknown or + * empty names, and successful construction of every whitelisted adapter. + */ +public class RateLimiterServletTest { + + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; + + @Test + public void testWhitelistContents() { + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testUnknownAdapterThrowsTronError() { + // Fail-fast parity with the pre-whitelist Class.forName behavior: an unknown + // adapter name raises TronError from @PostConstruct so Spring startup aborts + // rather than silently masking a misconfigured node. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("UnknownAdapter", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + assertTrue(e.getMessage().contains("UnknownAdapter")); + assertTrue(e.getMessage().contains("TestServlet")); + } + + @Test + public void testDefaultAdapterNameBuildsDefaultBaseQqsAdapter() { + // When no config entry exists for a servlet, addRateContainer passes + // DEFAULT_ADAPTER_NAME to buildAdapter; verify it resolves to + // DefaultBaseQqsAdapter. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyAdapterNameThrowsTronError() { + // Fail-fast parity with original: a configured-but-empty strategy name is + // a configuration bug and must not be silently replaced by the default. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + } + + @Test + public void testBuildsEachWhitelistedAdapter() { + // Exercises the newInstance(String) constructor path for every whitelisted + // adapter so a signature/strategy-class break on any entry fails here + // instead of at node startup. + assertTrue(RateLimiterServlet.buildAdapter( + QpsRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof QpsRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + IPQPSRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof IPQPSRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + GlobalPreemptibleAdapter.class.getSimpleName(), "permit=1", "TestServlet") + instanceof GlobalPreemptibleAdapter); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 221c5a7a165..7c05e0e9cfe 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -1,11 +1,15 @@ package org.tron.core.services.http; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -44,7 +48,7 @@ public void testPrintTransactionFee() { public void testPrintBlockList() { BlockCapsule blockCapsule1 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - BlockCapsule blockCapsule2 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + BlockCapsule blockCapsule2 = new BlockCapsule(2, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); GrpcAPI.BlockList list = GrpcAPI.BlockList.newBuilder() .addBlock(blockCapsule1.getInstance()) @@ -52,6 +56,148 @@ public void testPrintBlockList() { .build(); String out = Util.printBlockList(list, true); Assert.assertNotNull(out); + + JSONObject json = JSONObject.parseObject(out); + Assert.assertTrue(json.containsKey("block")); + JSONArray blockArray = json.getJSONArray("block"); + Assert.assertEquals(2, blockArray.size()); + + // verify each block has correct structure + for (int i = 0; i < blockArray.size(); i++) { + JSONObject blockJson = blockArray.getJSONObject(i); + Assert.assertTrue(blockJson.containsKey("blockID")); + Assert.assertTrue(blockJson.containsKey("block_header")); + Assert.assertFalse(blockJson.getString("blockID").isEmpty()); + JSONObject blockHeader = blockJson.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONEmptyTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertFalse(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + + @Test + public void testPrintBlockToJSONWithTransactions() { + // Structural invariants must hold under either visible flag; the flag-driven + // encoding difference is covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. + for (boolean visible : new boolean[]{true, false}) { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + blockCapsule.addTransaction(getTransactionCapsuleExample()); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), visible); + + String msg = "visible=" + visible; + Assert.assertTrue(msg, json.containsKey("blockID")); + Assert.assertTrue(msg, json.containsKey("block_header")); + Assert.assertTrue(msg, json.containsKey("transactions")); + Assert.assertFalse(msg, json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(msg, blockHeader); + Assert.assertTrue(msg, blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(msg, 1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(msg, txJson.containsKey("txID")); + Assert.assertTrue(msg, txJson.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { + // Pins the optimized printBlockToJSON against the prior behavior: the + // visible flag must still thread through to JsonFormat so address-bearing + // fields switch encoding while byte-identity fields stay stable. + ByteString witnessAddress = ByteString.copyFrom( + ByteArray.fromHexString("41548794500882809695a8a687866e76d4271a1abc")); + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), witnessAddress); + + JSONObject visible = Util.printBlockToJSON(blockCapsule.getInstance(), true); + JSONObject hidden = Util.printBlockToJSON(blockCapsule.getInstance(), false); + + // blockID is derived from raw bytes; identical under either flag. + Assert.assertEquals(visible.getString("blockID"), hidden.getString("blockID")); + + // Overall block_header must differ because witness_address is re-encoded. + String headerVisible = visible.getJSONObject("block_header").toJSONString(); + String headerHidden = hidden.getJSONObject("block_header").toJSONString(); + Assert.assertNotEquals(headerVisible, headerHidden); + + // visible=true renders a mainnet address as Base58 starting with 'T'. + String witnessVisible = visible.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessVisible); + Assert.assertTrue("visible=true witness_address should be Base58 ('T...'), got: " + + witnessVisible, witnessVisible.startsWith("T")); + + // visible=false keeps witness_address in raw (non-Base58) form. + String witnessHidden = hidden.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessHidden); + Assert.assertNotEquals(witnessVisible, witnessHidden); + } + + @Test + public void testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl() { + // Legacy impl produced JSON via JsonFormat.printToString(block, selfType), + // which omits repeated fields when empty. New impl mirrors that with an + // explicit isEmpty() guard. Pin parity using JsonFormat output as ground + // truth so a future refactor can't quietly start emitting "transactions": [] + // (or stop emitting the key when non-empty). + BlockCapsule empty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + assertTransactionsKeyMatchesLegacy(empty.getInstance(), false); + + BlockCapsule nonEmpty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + nonEmpty.addTransaction(getTransactionCapsuleExample()); + assertTransactionsKeyMatchesLegacy(nonEmpty.getInstance(), true); + } + + private static void assertTransactionsKeyMatchesLegacy(Protocol.Block block, + boolean expectTransactionsKey) { + JSONObject legacy = JSONObject.parseObject(JsonFormat.printToString(block, true)); + Assert.assertEquals("legacy JsonFormat parity broken — proto behavior changed?", + expectTransactionsKey, legacy.containsKey("transactions")); + + JSONObject actual = Util.printBlockToJSON(block, true); + Assert.assertEquals("new impl diverged from legacy on 'transactions' key presence", + expectTransactionsKey, actual.containsKey("transactions")); + } + + @Test + public void testPrintBlockToJSONCoversAllProtoTopLevelFields() { + // Guards against proto field drift: the old impl delegated to JsonFormat on + // the whole Block message, so any new top-level Block field appeared + // automatically. The new impl hand-assembles the JSON, so a future proto + // field would be silently dropped. Reflect over Block's descriptor and + // assert every declared top-level field is handled. + Map protoFieldToJsonKey = new HashMap<>(); + protoFieldToJsonKey.put("block_header", "block_header"); + // "transactions" is present only when non-empty; parity verified in + // testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl. + protoFieldToJsonKey.put("transactions", "transactions"); + + for (Descriptors.FieldDescriptor f : Protocol.Block.getDescriptor().getFields()) { + Assert.assertTrue( + "Block proto field '" + f.getName() + "' is not handled by printBlockToJSON. " + + "If you added a new top-level field, extend printBlockToJSON and this test.", + protoFieldToJsonKey.containsKey(f.getName())); + } } @Test diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java new file mode 100644 index 00000000000..6aaeea2cc4e --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -0,0 +1,78 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public class JsonRpcApiUtilTest { + + @Test + public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1aL, JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(0L, JsonRpcApiUtil.parseBlockNumber("0x0")); + } + + @Test + public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { + assertEquals(12345L, JsonRpcApiUtil.parseBlockNumber("12345")); + } + + @Test + public void parseBlockNumberAcceptsMaxLongValue() throws JsonRpcInvalidParamsException { + assertEquals(Long.MAX_VALUE, + JsonRpcApiUtil.parseBlockNumber("0x7fffffffffffffff")); + } + + @Test + public void parseBlockNumberRejectsNegative() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("-1")); + assertEquals("invalid block number", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x-1")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOverflow() { + // 2^64 - 1: fits uint64 but overflows signed long + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xffffffffffffffff")); + assertEquals("invalid block number", e1.getMessage()); + // 2^63: just past Long.MAX_VALUE + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x8000000000000000")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOversized() { + // 101 chars exceeds the 100-char limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(null)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xGG")); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsEmpty() { + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("")); + } +}