diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 8c86f2f66ac..394a683dda6 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -32,11 +32,6 @@ 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.parseEnergyFee; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; import static org.tron.core.vm.utils.FreezeV2Util.getV2EnergyUsage; import static org.tron.core.vm.utils.FreezeV2Util.getV2NetUsage; import static org.tron.protos.contract.Common.ResourceCode; @@ -193,7 +188,6 @@ import org.tron.core.exception.VMIllegalException; import org.tron.core.exception.ValidateSignatureException; import org.tron.core.exception.ZksnarkException; -import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.TronNetService; import org.tron.core.net.message.adv.TransactionMessage; @@ -711,6 +705,10 @@ public long getSolidBlockNum() { return chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); } + public long getHeadBlockNum() { + return chainBaseManager.getHeadBlockNum(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -733,37 +731,6 @@ public long getTransactionCountByBlockNum(long blockNum) { return count; } - public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(id)) { - return getBlockByNum(0); - } else if (LATEST_STR.equalsIgnoreCase(id)) { - return getNowBlock(); - } else if (FINALIZED_STR.equalsIgnoreCase(id)) { - return getSolidBlock(); - } 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"); - } - - return getBlockByNum(blockNumber); - } - } - - public List getTransactionsByJsonBlockId(String id) - throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(id)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); - } else { - Block block = getByJsonBlockId(id); - return block != null ? block.getTransactionsList() : null; - } - } - public WitnessList getWitnessList() { WitnessList.Builder builder = WitnessList.newBuilder(); List witnessCapsuleList = chainBaseManager.getWitnessStore().getAllWitnesses(); 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..08c1068e3a2 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 @@ -1,11 +1,5 @@ package org.tron.core.services.jsonrpc; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; - import com.google.common.base.Throwables; import com.google.common.primitives.Longs; import com.google.protobuf.Any; @@ -57,6 +51,15 @@ @Slf4j(topic = "API") public class JsonRpcApiUtil { + public static final String EARLIEST_STR = "earliest"; + public static final String PENDING_STR = "pending"; + public static final String LATEST_STR = "latest"; + public static final String FINALIZED_STR = "finalized"; + public static final String SAFE_STR = "safe"; + 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 byte[] convertToTronAddress(byte[] address) { byte[] newAddress = new byte[21]; byte[] temp = new byte[] {Wallet.getAddressPreFixByte()}; @@ -515,20 +518,52 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { return -1; } - public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) + public static boolean isBlockTag(String tag) { + return LATEST_STR.equalsIgnoreCase(tag) + || EARLIEST_STR.equalsIgnoreCase(tag) + || FINALIZED_STR.equalsIgnoreCase(tag) + || PENDING_STR.equalsIgnoreCase(tag) + || SAFE_STR.equalsIgnoreCase(tag); + } + + /** + * Parse a block tag (latest, earliest, finalized) to block number. + * + *

Note: for "latest", the returned block number may not yet be available in + * blockStore or blockIndexStore due to write ordering. Callers that need the + * actual block must handle the not-found case.

+ */ + public static long parseBlockTag(String tag, Wallet wallet) throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + if (LATEST_STR.equalsIgnoreCase(tag)) { + return wallet.getHeadBlockNum(); } - if (StringUtils.isEmpty(blockNumOrTag) || LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - return -1; - } else if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag)) { + if (EARLIEST_STR.equalsIgnoreCase(tag)) { return 0; - } else if (FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { + } + if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); - } else { - return ByteArray.jsonHexToLong(blockNumOrTag); } + if (PENDING_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + } + if (SAFE_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_SAFE_SUPPORT_ERROR); + } + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + + /** + * 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. + */ + public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) + throws JsonRpcInvalidParamsException { + if (isBlockTag(blockNumOrTag)) { + return parseBlockTag(blockNumOrTag, wallet); + } + return ByteArray.jsonHexToLong(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 de939bdfff4..72fc579aa56 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,6 +3,9 @@ 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.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; @@ -152,17 +155,11 @@ public enum RequestSource { public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; - public static final String EARLIEST_STR = "earliest"; - public static final String PENDING_STR = "pending"; - public static final String LATEST_STR = "latest"; - public static final String FINALIZED_STR = "finalized"; - public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; public static final String INVALID_BLOCK_RANGE = "invalid block range params"; private static final String JSON_ERROR = "invalid json request"; - private static final String BLOCK_NUM_ERROR = "invalid block number"; private static final String TAG_NOT_SUPPORT_ERROR = - "TAG [earliest | pending | finalized] not supported"; + "TAG [earliest | pending | finalized | safe] not supported"; private static final String QUANTITY_NOT_SUPPORT_ERROR = "QUANTITY not supported, just support TAG as latest"; private static final String NO_BLOCK_HEADER = "header not found"; @@ -308,12 +305,12 @@ public String ethGetBlockTransactionCountByHash(String blockHash) @Override public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) throws JsonRpcInvalidParamsException { - List list = wallet.getTransactionsByJsonBlockId(blockNumOrTag); - if (list == null) { + Block block = getBlockByNumOrTag(blockNumOrTag); + if (block == null) { return null; } - long n = list.size(); + long n = block.getTransactionsCount(); return ByteArray.toJsonHex(n); } @@ -327,7 +324,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) throws JsonRpcInvalidParamsException { - final Block b = wallet.getByJsonBlockId(blockNumOrTag); + final Block b = getBlockByNumOrTag(blockNumOrTag); return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } @@ -345,11 +342,49 @@ private byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException return bHash; } + /** + * Reject any block selector that is not "latest". + * Accepts "latest" silently; throws for other tags, numeric blocks, or invalid input. + */ + private void requireLatestBlockTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + return; + } + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); + } + try { + ByteArray.hexToBigInteger(blockNumOrTag); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + } + private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsException { byte[] bHash = hashToByteArray(blockHash); return wallet.getBlockById(ByteString.copyFrom(bHash)); } + private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + long blockNum; + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + // Return the head block directly from blockStore, bypassing blockIndexStore + // which may not yet be written when latestBlockHeaderNumber is already updated. + return wallet.getNowBlock(); + } + return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); + } + try { + blockNum = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + return wallet.getBlockByNum(blockNum); + } + private BlockResult getBlockResult(Block block, boolean fullTx) { if (block == null) { return null; @@ -393,30 +428,18 @@ public String getLatestBlockNum() { @Override public String getTrxBalance(String address, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(address); + requireLatestBlockTag(blockNumOrTag); - Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); - Account reply = wallet.getAccount(account); - long balance = 0; + byte[] addressData = addressCompatibleToByteArray(address); - if (reply != null) { - balance = reply.getBalance(); - } - return ByteArray.toJsonHex(balance); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); + Account reply = wallet.getAccount(account); + long balance = 0; - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + if (reply != null) { + balance = reply.getBalance(); } + return ByteArray.toJsonHex(balance); } private void callTriggerConstantContract(byte[] ownerAddressByte, byte[] contractAddressByte, @@ -535,67 +558,42 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va @Override public String getStorageAt(String address, String storageIdx, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressByte = addressCompatibleToByteArray(address); - - // get contract from contractStore - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); - SmartContract smartContract = wallet.getContract(bytesMessage); - if (smartContract == null) { - return ByteArray.toJsonHex(new byte[32]); - } + requireLatestBlockTag(blockNumOrTag); - StorageRowStore store = manager.getStorageRowStore(); - Storage storage = new Storage(addressByte, store); - storage.setContractVersion(smartContract.getVersion()); - storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + byte[] addressByte = addressCompatibleToByteArray(address); - 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); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + // get contract from contractStore + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); + SmartContract smartContract = wallet.getContract(bytesMessage); + if (smartContract == null) { + return ByteArray.toJsonHex(new byte[32]); } + + StorageRowStore store = manager.getStorageRowStore(); + Storage storage = new Storage(addressByte, store); + storage.setContractVersion(smartContract.getVersion()); + storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + + DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); + return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } @Override public String getABIOfSmartContract(String contractAddress, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(contractAddress); + requireLatestBlockTag(blockNumOrTag); - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); - SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + byte[] addressData = addressCompatibleToByteArray(contractAddress); - if (contractDataWrapper != null) { - return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); - } else { - return "0x"; - } + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); + SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + if (contractDataWrapper != null) { + return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + return "0x"; } } @@ -803,7 +801,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) throws JsonRpcInvalidParamsException { - Block block = wallet.getByJsonBlockId(blockNumOrTag); + Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } @@ -894,7 +892,7 @@ public List getBlockReceipts(String blockNumOrHashOrTag) if (Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { block = getBlockByJsonHash(blockNumOrHashOrTag); } else { - block = wallet.getByJsonBlockId(blockNumOrHashOrTag); + block = getBlockByNumOrTag(blockNumOrHashOrTag); } // block receipts not available: block is genesis, not produced yet, or pruned in light node @@ -973,7 +971,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) long blockNumber; try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); + blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); } catch (Exception e) { throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } @@ -1003,25 +1001,13 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidRequestException(JSON_ERROR); } - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); - byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); + requireLatestBlockTag(blockNumOrTag); - return call(addressData, contractAddressData, transactionCall.parseValue(), - ByteArray.fromHexString(transactionCall.getData())); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); + byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); - } + return call(addressData, contractAddressData, transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.getData())); } @Override 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 97a012b7f9a..0331ab3694a 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 @@ -1,6 +1,7 @@ package org.tron.core.services.jsonrpc.filters; import static org.tron.common.math.StrictMathWrapper.min; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import com.google.protobuf.ByteString; import lombok.Getter; @@ -50,39 +51,50 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet toBlockSrc = fromBlockSrc; } else { - // if fromBlock is empty but toBlock is not empty, - // then if toBlock < maxBlockNum, set fromBlock = toBlock - // then if toBlock >= maxBlockNum, set fromBlock = maxBlockNum - if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isNotEmpty(fr.getToBlock())) { - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (toBlockSrc == -1) { - toBlockSrc = Long.MAX_VALUE; - } - fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + // Normalize the request into one of four strategies based on parameter emptiness. + // Long.MAX_VALUE is an internal sentinel meaning "open upper bound"; it is never + // treated as a real block number by later query stages. + // Note: "latest" tag handling differs by strategy: + // - Strategy 2: toBlock="latest" -> Long.MAX_VALUE (track future blocks) + // - Strategy 3: fromBlock="latest" -> currentMaxBlockNum snapshot (bounded start) + // - Strategy 4: fromBlock="latest" -> currentMaxBlockNum; toBlock="latest" -> Long.MAX_VALUE - } else if (StringUtils.isNotEmpty(fr.getFromBlock()) - && StringUtils.isEmpty(fr.getToBlock())) { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - if (fromBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - } - toBlockSrc = Long.MAX_VALUE; + boolean fromEmpty = StringUtils.isEmpty(fr.getFromBlock()); + boolean toEmpty = StringUtils.isEmpty(fr.getToBlock()); - } else if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isEmpty(fr.getToBlock())) { + if (fromEmpty && toEmpty) { + // Strategy 1: Both parameters omitted. Start at the current head and track new blocks. fromBlockSrc = currentMaxBlockNum; toBlockSrc = Long.MAX_VALUE; - } else { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (fromBlockSrc == -1 && toBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - toBlockSrc = Long.MAX_VALUE; - } else if (fromBlockSrc == -1 && toBlockSrc >= 0) { - fromBlockSrc = currentMaxBlockNum; - } else if (fromBlockSrc >= 0 && toBlockSrc == -1) { + } else if (fromEmpty) { + // Strategy 2: Only toBlock specified. + // If toBlock is "latest": track future blocks (fromBlock = currentMaxBlockNum, + // toBlock = MAX_VALUE). If concrete: bounded query with fromBlock = min(toBlock, + // currentMaxBlockNum). + if (LATEST_STR.equalsIgnoreCase(fr.getToBlock())) { toBlockSrc = Long.MAX_VALUE; + } else { + toBlockSrc = JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); } + fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + + } else if (toEmpty) { + // Strategy 3: Only fromBlock specified. Start at fromBlock and track new blocks. + // If fromBlock is "latest", use the snapshot (currentMaxBlockNum) as the starting point. + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = Long.MAX_VALUE; + + } else { + // Strategy 4: Both parameters specified. + // If fromBlock is "latest": use the snapshot (currentMaxBlockNum) as a fixed start point. + // If toBlock is "latest": use Long.MAX_VALUE to track future blocks. + // Otherwise: parse both as concrete block numbers + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getToBlock()) ? Long.MAX_VALUE + : JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); if (fromBlockSrc > toBlockSrc) { throw new JsonRpcInvalidParamsException("please verify: fromBlock <= toBlock"); } 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 ced7048c9d2..d71ffbb980d 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1,7 +1,10 @@ package org.tron.core.jsonrpc; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getByJsonBlockId; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_SAFE_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.isBlockTag; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockTag; import com.alibaba.fastjson.JSON; import com.google.gson.JsonArray; @@ -10,6 +13,7 @@ import io.prometheus.client.CollectorRegistry; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -45,6 +49,7 @@ import org.tron.core.services.interfaceJsonRpcOnSolidity.JsonRpcServiceOnSolidity; import org.tron.core.services.jsonrpc.FullNodeJsonRpcHttpService; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; @@ -62,6 +67,8 @@ public class JsonrpcServiceTest extends BaseTest { private static final String OWNER_ADDRESS_ACCOUNT_NAME = "first"; private static final long LATEST_BLOCK_NUM = 10_000L; private static final long LATEST_SOLIDIFIED_BLOCK_NUM = 4L; + private static final String TAG_NOT_SUPPORT_ERROR = + "TAG [earliest | pending | finalized | safe] not supported"; private static TronJsonRpcImpl tronJsonRpc; @Resource @@ -282,6 +289,21 @@ public void testGetBlockTransactionCountByNumber() { } Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTransactions().size()), result); + // safe tag is not supported (new tag added in this refactor) + try { + tronJsonRpc.ethGetBlockTransactionCountByNumber("safe"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // hex that overflows long -> longValueExact rejects (previously silently truncated) + try { + tronJsonRpc.ethGetBlockTransactionCountByNumber("0x10000000000000000"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } } @Test @@ -359,6 +381,22 @@ public void testGetBlockByNumber() { } catch (Exception e) { Assert.assertEquals("invalid block number", e.getMessage()); } + + // safe + try { + tronJsonRpc.ethGetBlockByNumber("safe", false); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // hex overflows long -> longValueExact rejects + try { + tronJsonRpc.ethGetBlockByNumber("0x10000000000000000", false); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } } @Test @@ -429,53 +467,76 @@ public void testServicesInit() { } @Test - public void testGetByJsonBlockId() { - long blkNum = 0; + public void testBlockTagParsing() { + // isBlockTag + Assert.assertTrue(isBlockTag("pending")); + Assert.assertTrue(isBlockTag("latest")); + Assert.assertTrue(isBlockTag("earliest")); + Assert.assertTrue(isBlockTag("finalized")); + Assert.assertTrue(isBlockTag("safe")); + Assert.assertFalse(isBlockTag(null)); + Assert.assertFalse(isBlockTag("0xa")); + Assert.assertFalse(isBlockTag("")); + + // parseBlockTag: pending throws + try { + parseBlockTag("pending", wallet); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + // parseBlockTag: safe throws try { - getByJsonBlockId("pending", wallet); + parseBlockTag("safe", wallet); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); } + // parseBlockTag: latest -> headBlockNum try { - blkNum = getByJsonBlockId(null, wallet); + long blkNum = parseBlockTag("latest", wallet); + Assert.assertEquals(LATEST_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: earliest -> 0 try { - blkNum = getByJsonBlockId("latest", wallet); + long blkNum = parseBlockTag("earliest", wallet); + Assert.assertEquals(0L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: finalized -> solidBlockNum try { - blkNum = getByJsonBlockId("finalized", wallet); + long blkNum = parseBlockTag("finalized", wallet); + Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); + // parseBlockNumber: hex -> number try { - blkNum = getByJsonBlockId("0xa", wallet); + long blkNum = parseBlockNumber("0xa", wallet); + Assert.assertEquals(10L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(10L, blkNum); + // parseBlockNumber: bad hex -> throws try { - getByJsonBlockId("abc", wallet); + parseBlockNumber("abc", wallet); Assert.fail("Expected to be thrown"); } catch (Exception e) { Assert.assertEquals("Incorrect hex syntax", e.getMessage()); } + // parseBlockNumber: malformed hex -> throws try { - getByJsonBlockId("0xxabc", wallet); + parseBlockNumber("0xxabc", wallet); Assert.fail("Expected to be thrown"); } catch (Exception e) { // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed @@ -491,7 +552,7 @@ public void testGetTrxBalance() { tronJsonRpc.getTrxBalance("", "earliest"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -499,7 +560,7 @@ public void testGetTrxBalance() { tronJsonRpc.getTrxBalance("", "pending"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -507,7 +568,15 @@ public void testGetTrxBalance() { tronJsonRpc.getTrxBalance("", "finalized"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, + e.getMessage()); + } + + try { + tronJsonRpc.getTrxBalance("", "safe"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -526,7 +595,7 @@ public void testGetStorageAt() { tronJsonRpc.getStorageAt("", "", "earliest"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -534,7 +603,7 @@ public void testGetStorageAt() { tronJsonRpc.getStorageAt("", "", "pending"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -542,9 +611,43 @@ public void testGetStorageAt() { tronJsonRpc.getStorageAt("", "", "finalized"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, + e.getMessage()); + } + + try { + tronJsonRpc.getStorageAt("", "", "safe"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + try { + tronJsonRpc.getStorageAt("", "", "0x1"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e.getMessage()); + } + + // malformed hex -> BLOCK_NUM_ERROR + try { + tronJsonRpc.getStorageAt("", "", "abc"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + + // latest happy path: address is an account, not a contract, so returns 32 zero bytes + try { + String value = tronJsonRpc.getStorageAt( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "0x0", "latest"); + Assert.assertEquals(ByteArray.toJsonHex(new byte[32]), value); + } catch (Exception e) { + Assert.fail(); + } } @Test @@ -553,7 +656,7 @@ public void testGetABIOfSmartContract() { tronJsonRpc.getABIOfSmartContract("", "earliest"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -561,7 +664,7 @@ public void testGetABIOfSmartContract() { tronJsonRpc.getABIOfSmartContract("", "pending"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -569,9 +672,43 @@ public void testGetABIOfSmartContract() { tronJsonRpc.getABIOfSmartContract("", "finalized"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } + + try { + tronJsonRpc.getABIOfSmartContract("", "safe"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, + e.getMessage()); + } + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + try { + tronJsonRpc.getABIOfSmartContract("", "0x1"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e.getMessage()); + } + + // malformed hex -> BLOCK_NUM_ERROR + try { + tronJsonRpc.getABIOfSmartContract("", "abc"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + + // latest happy path: address is an account, not a contract, so returns "0x" + try { + String code = tronJsonRpc.getABIOfSmartContract( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "latest"); + Assert.assertEquals("0x", code); + } catch (Exception e) { + Assert.fail(); + } } @Test @@ -580,7 +717,7 @@ public void testGetCall() { tronJsonRpc.getCall(null, "earliest"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -588,7 +725,7 @@ public void testGetCall() { tronJsonRpc.getCall(null, "pending"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } @@ -596,11 +733,171 @@ public void testGetCall() { tronJsonRpc.getCall(null, "finalized"); Assert.fail("Expected to be thrown"); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, + e.getMessage()); + } + + try { + tronJsonRpc.getCall(null, "safe"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e.getMessage()); } } + @Test + public void testGetTransactionByBlockNumberAndIndex() { + // valid hex block number: blockCapsule1 has 2 txs; index 0 is transactionCapsule1. + // Assert the returned tx actually resolves to transactionCapsule1's hash, + // block number, and index rather than just non-null. + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals( + ByteArray.toJsonHex(transactionCapsule1.getTransactionId().getBytes()), + result.getHash()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); + Assert.assertEquals(ByteArray.toJsonHex(0L), result.getTransactionIndex()); + } catch (Exception e) { + Assert.fail(); + } + + // index out of range in an existing block returns null + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x5"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // latest -> blockCapsule1 (head) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("latest", "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); + } catch (Exception e) { + Assert.fail(); + } + + // finalized -> blockCapsule2 (solid), has 1 tx + try { + TransactionResult result = + tronJsonRpc.getTransactionByBlockNumberAndIndex("finalized", "0x0"); + Assert.assertNotNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // non-existent block number returns null (not an error) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("0x1", "0x0"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // pending tag rejected + try { + tronJsonRpc.getTransactionByBlockNumberAndIndex("pending", "0x0"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + + // safe tag rejected (new tag) + try { + tronJsonRpc.getTransactionByBlockNumberAndIndex("safe", "0x0"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // malformed hex rejected + try { + tronJsonRpc.getTransactionByBlockNumberAndIndex("qqq", "0x0"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + + // hex overflows long -> longValueExact rejects + try { + tronJsonRpc.getTransactionByBlockNumberAndIndex("0x10000000000000000", "0x0"); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + } + + /** + * Tests the object-form second argument of eth_call: + * {"blockNumber": "0x..."} or {"blockHash": "0x..."}. + * Only the block-selector parsing is exercised here; the call() + * execution path is covered by other tests. + */ + @Test + public void testGetCallWithBlockObject() { + // neither HashMap nor String -> invalid json request + try { + tronJsonRpc.getCall(null, new Object()); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid json request", e.getMessage()); + } + + // HashMap without blockNumber/blockHash keys -> invalid json request + try { + tronJsonRpc.getCall(null, new HashMap()); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid json request", e.getMessage()); + } + + // blockNumber with malformed hex -> invalid block number + try { + HashMap params = new HashMap<>(); + params.put("blockNumber", "xxx"); + tronJsonRpc.getCall(null, params); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + + // blockNumber overflows long -> invalid block number (longValueExact) + try { + HashMap params = new HashMap<>(); + params.put("blockNumber", "0x10000000000000000"); + tronJsonRpc.getCall(null, params); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } + + // blockNumber points to a non-existent block -> header not found + try { + HashMap params = new HashMap<>(); + params.put("blockNumber", "0x1"); + tronJsonRpc.getCall(null, params); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("header not found", e.getMessage()); + } + + // blockHash of an unknown block -> header for hash not found + try { + HashMap params = new HashMap<>(); + params.put("blockHash", + "0x1111111111111111111111111111111111111111111111111111111111111111"); + tronJsonRpc.getCall(null, params); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals("header for hash not found", e.getMessage()); + } + } + /** * test fromBlock and toBlock parameters */ @@ -1014,6 +1311,115 @@ public void testNewFilterFinalizedBlock() { } } + /** + * Tag handling at the RPC boundary for eth_newFilter / eth_getLogs / eth_getFilterLogs. + * - safe/pending are rejected inside LogFilterWrapper (parseBlockNumber -> parseBlockTag) + * - finalized is intercepted by newFilter's upfront guard, but allowed by getLogs + * - getFilterLogs round-trips a filter created with concrete block numbers + */ + @Test + public void testLogFilterTagHandling() { + // eth_newFilter: safe in fromBlock -> TAG_SAFE_SUPPORT_ERROR + try { + tronJsonRpc.newFilter(new FilterRequest("safe", null, null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // eth_newFilter: safe in toBlock + try { + tronJsonRpc.newFilter(new FilterRequest("0x1", "safe", null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // eth_newFilter: pending in fromBlock + try { + tronJsonRpc.newFilter(new FilterRequest("pending", null, null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + + // eth_newFilter: pending in toBlock + try { + tronJsonRpc.newFilter(new FilterRequest("0x1", "pending", null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + + // eth_getLogs: safe in fromBlock + try { + tronJsonRpc.getLogs(new FilterRequest("safe", null, null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // eth_getLogs: safe in toBlock + try { + tronJsonRpc.getLogs(new FilterRequest(null, "safe", null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + // eth_getLogs: pending in fromBlock + try { + tronJsonRpc.getLogs(new FilterRequest("pending", null, null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + + // eth_getLogs: pending in toBlock + try { + tronJsonRpc.getLogs(new FilterRequest(null, "pending", null, null, null)); + Assert.fail("Expected to be thrown"); + } catch (Exception e) { + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); + } + + // eth_getLogs: finalized is accepted (resolves to solidBlockNum via parseBlockTag). + // With fromBlock empty, Strategy 2 resolves the range to [solid, solid]. blockCapsule2 + // (solid=4) has no logs in test fixtures, so result must be empty. + try { + LogFilterElement[] result = + tronJsonRpc.getLogs(new FilterRequest(null, "finalized", null, null, null)); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + + // End-to-end happy path for eth_getLogs and eth_getFilterLogs. + // Query range [head, head] = [blockCapsule1, blockCapsule1]. No address/topic filter, + // so LogBlockQuery marks all blocks in the range as candidates. LogMatch then iterates + // blockCapsule1's 2 txs * 2 logs each = 4 LogFilterElements. + String headHex = ByteArray.toJsonHex(blockCapsule1.getNum()); + int expectedLogs = blockCapsule1.getTransactions().size() * 2; + + try { + LogFilterElement[] directResult = + tronJsonRpc.getLogs(new FilterRequest(headHex, headHex, null, null, null)); + Assert.assertEquals(expectedLogs, directResult.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + + try { + String filterIdHex = tronJsonRpc.newFilter( + new FilterRequest(headHex, headHex, null, null, null)); + LogFilterElement[] filterResult = tronJsonRpc.getFilterLogs(filterIdHex); + Assert.assertEquals(expectedLogs, filterResult.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + @Test public void testGetBlockReceipts() { @@ -1087,6 +1493,19 @@ public void testGetBlockReceipts() { throw new RuntimeException(e); } + try { + tronJsonRpc.getBlockReceipts("safe"); + Assert.fail(); + } catch (Exception e) { + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e.getMessage()); + } + + try { + tronJsonRpc.getBlockReceipts("0x10000000000000000"); + Assert.fail(); + } catch (Exception e) { + Assert.assertEquals("invalid block number", e.getMessage()); + } } @Test diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java new file mode 100644 index 00000000000..02fca2ecdd5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java @@ -0,0 +1,170 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.Wallet; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; + +/** + * Verify LogFilterWrapper strategies match develop branch behavior. + * + * Four filter strategies based on parameter emptiness (develop branch semantics): + * - Strategy 1: Both fromBlock and toBlock are empty -> (currentMaxBlockNum, Long.MAX_VALUE) + * - Strategy 2: fromBlock empty, toBlock non-empty -> based on toBlock value + * - Strategy 3: fromBlock non-empty, toBlock empty -> (fromBlock, Long.MAX_VALUE) + * - Strategy 4: Both non-empty -> parse both, handle "latest" using snapshot + */ +public class LogFilterWrapperStrategyTest { + + private Wallet mockWallet; + private static final long CURRENT_MAX_BLOCK = 81628775L; + + @Before + public void setUp() { + mockWallet = mock(Wallet.class); + when(mockWallet.getHeadBlockNum()).thenReturn(CURRENT_MAX_BLOCK); + when(mockWallet.getSolidBlockNum()).thenReturn(CURRENT_MAX_BLOCK - 100); + } + + private LogFilterWrapper createFilter(String fromBlock, String toBlock) throws Exception { + FilterRequest request = new FilterRequest(fromBlock, toBlock, null, null, null); + return new LogFilterWrapper(request, CURRENT_MAX_BLOCK, mockWallet, false); + } + + // ============ Strategy 1: Both empty ============ + + @Test + public void testStrategy1_BothNull() throws Exception { + LogFilterWrapper filter = createFilter(null, null); + assertEquals("fromBlock should be currentMaxBlockNum", CURRENT_MAX_BLOCK, + filter.getFromBlock()); + assertEquals("toBlock should be Long.MAX_VALUE", Long.MAX_VALUE, + filter.getToBlock()); + } + + @Test + public void testStrategy1_BothEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("", ""); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + // ============ Strategy 2: fromBlock empty, toBlock non-empty ============ + + @Test + public void testStrategy2_FromEmptyToHex() throws Exception { + // toBlock = 0x100 = 256 + // fromBlock = min(256, CURRENT_MAX_BLOCK) = 256 + LogFilterWrapper filter = createFilter(null, "0x100"); + assertEquals(256L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyToLatest() throws Exception { + // toBlock = "latest" is treated as Long.MAX_VALUE in Strategy 2 + // fromBlock = min(Long.MAX_VALUE, CURRENT_MAX_BLOCK) = CURRENT_MAX_BLOCK + LogFilterWrapper filter = createFilter(null, "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyStringToHex() throws Exception { + LogFilterWrapper filter = createFilter("", "0x200"); + assertEquals(512L, filter.getFromBlock()); + assertEquals(512L, filter.getToBlock()); + } + + // ============ Strategy 3: fromBlock non-empty, toBlock empty ============ + + @Test + public void testStrategy3_FromHexToEmpty() throws Exception { + // fromBlock = 0x1 = 1 + // toBlock = Long.MAX_VALUE (tracking future blocks) + LogFilterWrapper filter = createFilter("0x1", null); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromLatestToEmpty() throws Exception { + // fromBlock = "latest" (using snapshot) = currentMaxBlockNum + // toBlock = Long.MAX_VALUE + LogFilterWrapper filter = createFilter("latest", null); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromHexToEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("0x5", ""); + assertEquals(5L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + // ============ Strategy 4: Both non-empty ============ + + @Test + public void testStrategy4_BothHex() throws Exception { + // fromBlock = 1, toBlock = 256 + LogFilterWrapper filter = createFilter("0x1", "0x100"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy4_BothLatest() throws Exception { + // Both "latest" are non-empty, so Strategy 4. + // fromBlock "latest" -> currentMaxBlockNum (snapshot). toBlock "latest" -> Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("latest", "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromHexToLatest() throws Exception { + // fromBlock = 0x1 (concrete). toBlock = "latest" resolves to Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("0x1", "latest"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromLatestToHexAboveLatest() throws Exception { + // This test requires a toBlock value larger than currentMaxBlockNum + // Using 0x5000000 (83886080) which is > 81628775 + LogFilterWrapper filter = createFilter("latest", "0x5000000"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(83886080L, filter.getToBlock()); + } + + @Test + public void testStrategy4_InvertedRangeThrows() throws Exception { + // fromBlock (0x100 = 256) > toBlock (0x1 = 1) should throw + try { + createFilter("0x100", "0x1"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } + + @Test + public void testStrategy4_LatestGreaterThanSmallBlock_Throws() throws Exception { + // fromBlock = "latest" (currentMaxBlockNum = 81628775) > toBlock (0x100 = 256) should throw + try { + createFilter("latest", "0x100"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } +}