From c164330e8a8cca52e03bdd78813a9773da8eb527 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Wed, 23 Sep 2026 12:41:56 +0800 Subject: [PATCH 1/7] feat(jsonrpc): return code 3 for revert and 4444 for pruned history - eth_call / eth_estimateGas: contract revert changes from -32000 to 3 - data a node does not have (pruned on a LiteNode, or receipts with transHistory.switch off) returns 4444 "Pruned history unavailable" --- .../java/org/tron/core/ChainBaseManager.java | 22 + .../capsule/TransactionResultCapsule.java | 6 +- .../tron/core/store/TransactionRetStore.java | 20 + .../JsonRpcExecutionRevertedException.java | 12 + .../JsonRpcPrunedHistoryException.java | 17 + .../src/main/java/org/tron/core/Wallet.java | 17 + .../main/java/org/tron/core/db/Manager.java | 1 + .../core/services/jsonrpc/JsonRpcApiUtil.java | 55 ++- .../core/services/jsonrpc/TronJsonRpc.java | 30 +- .../services/jsonrpc/TronJsonRpcImpl.java | 112 +++-- .../jsonrpc/filters/LogFilterAndResult.java | 3 +- .../jsonrpc/filters/LogFilterWrapper.java | 10 +- .../org/tron/core/utils/ResultCodeUtil.java | 48 ++ .../tron/core/db/TransactionRetStoreTest.java | 52 ++- .../core/jsonrpc/HandleLogsFilterTest.java | 13 +- .../JsonRpcCallAndEstimateGasTest.java | 153 +++++-- .../jsonrpc/JsonRpcPrunedHistoryTest.java | 418 ++++++++++++++++++ .../org/tron/core/jsonrpc/JsonRpcTest.java | 5 +- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 67 +-- .../core/jsonrpc/LogMatchOverLimitTest.java | 13 +- .../core/jsonrpc/SectionBloomStoreTest.java | 16 +- .../jsonrpc/JsonRpcErrorResolverTest.java | 38 +- .../tron/core/utils/ResultCodeUtilTest.java | 66 +++ 23 files changed, 1046 insertions(+), 148 deletions(-) create mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java create mode 100644 common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java create mode 100644 framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java create mode 100644 framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java create mode 100644 framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 21f0bac8d77..2fd05c8b73e 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -9,8 +9,10 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.metric.DbStatService; import org.tron.common.utils.ForkController; import org.tron.common.utils.Sha256Hash; @@ -244,6 +246,11 @@ public class ChainBaseManager { @Setter private long lowestBlockNum = -1; // except num = 0. + // lowest block with receipts; above lowestBlockNum on a LiteNode + @Getter + @Setter + private long lowestReceiptBlockNum = -1; + @Getter @Setter private long latestSaveBlockTime; @@ -397,6 +404,21 @@ private void init() { this.latestSaveBlockTime = System.currentTimeMillis(); } + /** + * Probes the receipt floor from the store itself, not from snapshot metadata; an empty + * store means receipts begin with the next executed block. With receipt persistence off + * the store never grows, so no floor exists. Must run after checkpoint recovery (so the + * last session's tail is visible) and before any session is built ({@code getNext} does + * not merge in-flight layers). + */ + public void probeLowestReceiptBlockNum() { + boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() + .getStorage().getTransactionHistorySwitch()); + this.lowestReceiptBlockNum = persistReceipts + ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) + : Long.MAX_VALUE; + } + public void shutdown() { dbStatService.shutdown(); } diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java index 8ff3064b73c..2f1b1ee80b6 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java @@ -147,6 +147,10 @@ public void setErrorCode(Result.code code) { this.transactionResult = this.transactionResult.toBuilder().setRet(code).build(); } + public void setResultCode(contractResult code) { + this.transactionResult = this.transactionResult.toBuilder().setContractRet(code).build(); + } + public long getShieldedTransactionFee() { return transactionResult.getShieldedTransactionFee(); } @@ -184,4 +188,4 @@ public byte[] getData() { public Result getInstance() { return this.transactionResult; } -} \ No newline at end of file +} diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index a22e69e8692..6b08c9dd69b 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -1,7 +1,10 @@ package org.tron.core.store; +import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; +import java.util.Map; import java.util.Objects; +import java.util.OptionalLong; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +40,23 @@ public void put(byte[] key, TransactionRetCapsule item) { } } + /** + * Lowest block number that has receipts, or empty when the store has none. On a LiteNode + * this is generally above the block floor: a snapshot ships block bodies but no receipts. + * + *

Startup probe only — must run before any session is built. With in-flight snapshot + * layers, {@code getNext} does not merge deletions correctly. + */ + public OptionalLong getLowestBlockNum() { + Map entries = revokingDB.getNext(ByteArray.fromLong(0), 1); + for (byte[] key : entries.keySet()) { + if (key.length == Long.BYTES) { + return OptionalLong.of(Longs.fromByteArray(key)); + } + } + return OptionalLong.empty(); + } + public TransactionInfoCapsule getTransactionInfo(byte[] key) throws BadItemException { long blockNumber = transactionStore.getBlockNumber(key); if (blockNumber == -1) { diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java new file mode 100644 index 00000000000..5a090e76705 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java @@ -0,0 +1,12 @@ +package org.tron.core.exception.jsonrpc; + +public class JsonRpcExecutionRevertedException extends JsonRpcException { + + public JsonRpcExecutionRevertedException(String message) { + super(message); + } + + public JsonRpcExecutionRevertedException(String message, Object data) { + super(message, data); + } +} diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java new file mode 100644 index 00000000000..d5cf4f0c887 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java @@ -0,0 +1,17 @@ +package org.tron.core.exception.jsonrpc; + +/** + * Thrown when a request targets historical state that a LiteNode has pruned. + * Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized + * by the Ethereum Execution API (EIP-4444). + */ +public class JsonRpcPrunedHistoryException extends JsonRpcException { + + public JsonRpcPrunedHistoryException(String message) { + super(message); + } + + public JsonRpcPrunedHistoryException(String message, Object data) { + super(message, data); + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index ac54cb2b7ff..f1d24740269 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -201,6 +201,7 @@ import org.tron.core.store.StoreFactory; import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; +import org.tron.core.utils.ResultCodeUtil; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; @@ -236,6 +237,7 @@ import org.tron.protos.Protocol.Transaction.Contract; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; import org.tron.protos.contract.BalanceContract; @@ -737,6 +739,18 @@ public long getHeadBlockNum() { return chainBaseManager.getHeadBlockNum(); } + public boolean isLiteNode() { + return chainBaseManager.isLiteNode(); + } + + public long getLowestBlockNum() { + return chainBaseManager.getLowestBlockNum(); + } + + public long getLowestReceiptBlockNum() { + return chainBaseManager.getLowestReceiptBlockNum(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -3186,12 +3200,15 @@ public Transaction callConstantContract(TransactionCapsule trxCap, ret.setStatus(0, code.SUCESS); if (StringUtils.isNoneEmpty(result.getRuntimeError())) { ret.setStatus(0, code.FAILED); + // same failure classification as executed transactions + ret.setResultCode(ResultCodeUtil.resolve(result.getException())); retBuilder .setMessage(ByteString.copyFromUtf8(result.getRuntimeError())) .build(); } if (result.isRevert()) { ret.setStatus(0, code.FAILED); + ret.setResultCode(contractResult.REVERT); retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed")) .build(); } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 9d7a7c979b9..8c626f9b0ab 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -496,6 +496,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); + chainBaseManager.probeLowestReceiptBlockNum(); transactionCache.initCache(); rewardViCalService.init(); this.setProposalController(ProposalController.createInstance(this)); 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 f4bba9fbf37..ce183a81f45 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 @@ -24,6 +24,7 @@ import org.tron.common.utils.StringUtil; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; @@ -62,6 +63,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 PRUNED_HISTORY_ERROR = "Pruned history unavailable"; public static final String TX_INDEX_ERROR = "invalid index value"; private static final SecureRandom random = new SecureRandom(); @@ -636,7 +638,7 @@ public static long parseBlockTag(String tag, Wallet wallet) return wallet.getHeadBlockNum(); } if (EARLIEST_STR.equalsIgnoreCase(tag)) { - return 0; + return earliestAvailableBlock(wallet); } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -700,6 +702,57 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) return parseBlockNumber(blockNumOrTag); } + /** + * The lowest block for which everything the node persists is available; what "earliest" + * resolves to and what a 4444 error carries in {@code data}. On a LiteNode this is the + * receipt floor when receipts are persisted, otherwise the body floor (receipt endpoints + * answer 4444 on such a node regardless of this value). On a FullNode it is genesis. + */ + public static long earliestAvailableBlock(Wallet wallet) { + if (!wallet.isLiteNode()) { + return 0; + } + long receiptFloor = wallet.getLowestReceiptBlockNum(); + return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor; + } + + /** + * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444. + * Raw primitive — no genesis exemption; callers own that semantics. + */ + public static void checkPrunedHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { + throw prunedHistory(earliestAvailableBlock(wallet)); + } + } + + /** + * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read + * receipts or logs; their floor is the first block with receipts. Same raw-primitive + * contract. Receipt persistence is a per-node switch independent of node type, so a node + * that never persists receipts is rejected before the LiteNode gate. + */ + public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + long receiptFloor = wallet.getLowestReceiptBlockNum(); + if (receiptFloor == Long.MAX_VALUE) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + if (wallet.isLiteNode() && blockNum < receiptFloor) { + throw prunedHistory(receiptFloor); + } + } + + /** + * The Execution API fixes the message verbatim; the earliest available block travels in + * {@code data} so a client can pick a fallback node from it. + */ + private static JsonRpcPrunedHistoryException prunedHistory(long earliestAvailable) { + return new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR, + "0x" + Long.toHexString(earliestAvailable)); + } + /** * 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. diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 50da763b8b9..6e03361e3ae 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java @@ -19,10 +19,12 @@ import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException; import org.tron.core.services.jsonrpc.types.BlockResult; import org.tron.core.services.jsonrpc.types.BuildArguments; @@ -55,8 +57,10 @@ public interface TronJsonRpc { @JsonRpcMethod("eth_getBlockTransactionCountByNumber") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) - String ethGetBlockTransactionCountByNumber(String bnOrId) throws JsonRpcInvalidParamsException; + String ethGetBlockTransactionCountByNumber(String bnOrId) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getBlockByHash") @JsonRpcErrors({ @@ -68,9 +72,10 @@ BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionObjects) @JsonRpcMethod("eth_getBlockByNumber") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) BlockResult ethGetBlockByNumber(String bnOrId, Boolean fullTransactionObjects) - throws JsonRpcInvalidParamsException; + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("net_version") String getNetVersion() throws JsonRpcInternalException; @@ -120,10 +125,12 @@ String getABIOfSmartContract(String contractAddress, String bnOrId) @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException; + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException; @JsonRpcMethod("eth_getTransactionByHash") @JsonRpcErrors({ @@ -141,9 +148,10 @@ TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String ind @JsonRpcMethod("eth_getTransactionByBlockNumberAndIndex") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) - throws JsonRpcInvalidParamsException; + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getTransactionReceipt") @JsonRpcErrors({ @@ -154,20 +162,23 @@ TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, Stri @JsonRpcMethod("eth_getBlockReceipts") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}") }) List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException; + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_call") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String getCall(CallArguments transactionCall, Object blockNumOrTag) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, - JsonRpcInternalException; + JsonRpcInternalException, JsonRpcExecutionRevertedException; @JsonRpcMethod("net_peerCount") String getPeerCount(); @@ -292,9 +303,10 @@ CompilationResult ethSubmitHashrate(String hashrate, String id) @JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), @JsonRpcError(exception = JsonRpcExceedLimitException.class, code = -32005, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, - JsonRpcMethodNotFoundException, JsonRpcExceedLimitException; + JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_newBlockFilter") @JsonRpcErrors({ @@ -327,6 +339,7 @@ Object[] getFilterChanges(String filterId) @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), @JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"), @JsonRpcError(exception = JsonRpcTooManyResultException.class, code = -32005, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), @JsonRpcError(exception = BadItemException.class, code = -32000, data = "{}"), @JsonRpcError(exception = ExecutionException.class, code = -32000, data = "{}"), @JsonRpcError(exception = InterruptedException.class, code = -32000, data = "{}"), @@ -334,7 +347,8 @@ Object[] getFilterChanges(String filterId) }) LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException, ExecutionException, InterruptedException, BadItemException, ItemNotFoundException, - JsonRpcMethodNotFoundException, JsonRpcTooManyResultException; + JsonRpcMethodNotFoundException, JsonRpcTooManyResultException, + JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getFilterLogs") @JsonRpcErrors({ 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 6be47886117..c2d933b277c 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 @@ -71,10 +71,12 @@ import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.VMIllegalException; import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.http.JsonFormat; @@ -101,6 +103,7 @@ import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.TransferAssetContract; import org.tron.protos.contract.BalanceContract.TransferContract; @@ -349,7 +352,7 @@ public String ethGetBlockTransactionCountByHash(String blockHash) @Override public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; @@ -368,7 +371,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { final Block b = getBlockByNumOrTag(blockNumOrTag); return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } @@ -394,16 +397,26 @@ private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsEx return wallet.getBlockById(ByteString.copyFrom(bHash)); } - private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + private Block getBlockByNumOrTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { + 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)); + blockNum = JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet); + } else { + blockNum = parseBlockNumber(blockNumOrTag); } - return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag)); + // Reject a pruned height before touching any store, so a LiteNode pays no lookup for + // history it cannot serve. Genesis is exempt: a snapshot copies block 0 explicitly, and + // lowestBlockNum is computed from block 1 upwards, so block 0 is always retained. + if (blockNum > 0) { + JsonRpcApiUtil.checkPrunedHistory(blockNum, wallet); + } + return wallet.getBlockByNum(blockNum); } private BlockResult getBlockResult(Block block, boolean fullTx) { @@ -543,12 +556,33 @@ static String tryDecodeRevertReason(byte[] resData) { } } + /** + * Rejects a failed constant-call execution: throws code 3 with the revert payload in data + * for a contract revert, -32000 for any other execution failure. + */ + private void requireExecutionSuccess(TransactionExtention.Builder trxExtBuilder, + Return.Builder retBuilder) + throws JsonRpcInternalException, JsonRpcExecutionRevertedException { + Transaction.Result txResult = trxExtBuilder.getTransaction().getRet(0); + if (txResult.getRet().equals(code.SUCESS)) { + return; + } + byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); + if (txResult.getContractRet() == contractResult.REVERT) { + throw new JsonRpcExecutionRevertedException(errMsg, ByteArray.toJsonHex(resData)); + } + throw new JsonRpcInternalException(errMsg, + resData.length > 0 ? ByteArray.toJsonHex(resData) : null); + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 */ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long value, - byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException { + byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); Return.Builder retBuilder = Return.newBuilder(); @@ -577,27 +611,14 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va trxExt = trxExtBuilder.build(); } - String result; - if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.SUCESS)) { - List list = trxExt.getConstantResultList(); - byte[] listBytes = new byte[0]; - for (ByteString bs : list) { - listBytes = ByteUtil.merge(listBytes, bs.toByteArray()); - } - result = ByteArray.toJsonHex(listBytes); - } else { - byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); - - if (resData.length > 0) { - throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); - } else { - throw new JsonRpcInternalException(errMsg); - } + requireExecutionSuccess(trxExtBuilder, retBuilder); + List list = trxExt.getConstantResultList(); + byte[] listBytes = new byte[0]; + for (ByteString bs : list) { + listBytes = ByteUtil.merge(listBytes, bs.toByteArray()); } - - return result; + return ByteArray.toJsonHex(listBytes); } @Override @@ -672,7 +693,8 @@ public String gasPrice() { @Override public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException { + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { byte[] ownerAddress = addressCompatibleToByteArray(args.getFrom()); ContractType contractType = args.getContractType(wallet); @@ -730,25 +752,12 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept throw new JsonRpcInternalException(errString); } - if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); - - if (data.length > 0) { - throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); - } else { - throw new JsonRpcInternalException(errMsg); - } - - } else { - - if (supportEstimateEnergy) { - return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired()); - } else { - return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed()); - } + requireExecutionSuccess(trxExtBuilder, retBuilder); + if (supportEstimateEnergy) { + return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired()); } + return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed()); } @Override @@ -844,7 +853,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; @@ -929,7 +938,8 @@ private TransactionContext findTransactionContext(TransactionInfoList infoList, */ @Override public List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException { + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException { Block block = null; @@ -947,13 +957,15 @@ public List getBlockReceipts(String blockNumOrHashOrTag) BlockCapsule blockCapsule = new BlockCapsule(block); long blockNum = blockCapsule.getNum(); + int transactionSizeInBlock = blockCapsule.getTransactions().size(); + // below the receipt floor the body exists but the receipts do not — 4444, not -32000 + JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet); TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum); // energy price at the block timestamp long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp()); // Validate transaction list size consistency - int transactionSizeInBlock = blockCapsule.getTransactions().size(); if (transactionSizeInBlock != transactionInfoList.getTransactionInfoCount()) { throw new JsonRpcInternalException( String.format("TransactionList size mismatch: " @@ -1000,7 +1012,7 @@ private List getTransactionReceiptsFromBlock(BlockCapsule bl @Override public String getCall(CallArguments transactionCall, Object blockParamObj) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, - JsonRpcInternalException { + JsonRpcInternalException, JsonRpcExecutionRevertedException { String blockNumOrTag; if (blockParamObj instanceof HashMap) { @@ -1438,7 +1450,8 @@ public CompilationResult ethSubmitHashrate(String hashrate, String id) @Override public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, - JsonRpcMethodNotFoundException, JsonRpcExceedLimitException { + JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, + JsonRpcPrunedHistoryException { disableInPBFT("eth_newFilter"); // not supports finalized as block parameter @@ -1537,7 +1550,8 @@ public Object[] getFilterChanges(String filterId) throws ItemNotFoundException, @Override public LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException, ExecutionException, InterruptedException, BadItemException, ItemNotFoundException, - JsonRpcMethodNotFoundException, JsonRpcTooManyResultException { + JsonRpcMethodNotFoundException, JsonRpcTooManyResultException, + JsonRpcPrunedHistoryException { disableInPBFT("eth_getLogs"); long currentMaxBlockNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java index 57739819d1e..6c3296e5c0c 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java @@ -6,6 +6,7 @@ import lombok.Getter; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; @@ -15,7 +16,7 @@ public class LogFilterAndResult extends FilterResult { private final LogFilterWrapper logFilterWrapper; public LogFilterAndResult(FilterRequest fr, long currentMaxBlockNum, Wallet wallet) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // eth_newFilter, no need to check block range this.logFilterWrapper = new LogFilterWrapper(fr, currentMaxBlockNum, wallet, false); result = new LinkedBlockingQueue<>(); 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 0fdf174bb50..4d3871d5479 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 @@ -9,6 +9,7 @@ import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.JsonRpcApiUtil; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.protos.Protocol.Block; @@ -25,7 +26,8 @@ public class LogFilterWrapper { private final long toBlock; public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet, - boolean checkBlockRange) throws JsonRpcInvalidParamsException { + boolean checkBlockRange) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // 1.convert FilterRequest to LogFilter this.logFilter = new LogFilter(fr); @@ -103,6 +105,12 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet this.fromBlock = fromBlockSrc; this.toBlock = toBlockSrc; + // Reject a range starting below the receipt floor with 4444. Exception: a genesis-only + // query (from = to = 0, or the genesis blockHash) — block 0 is retained. + if (wallet != null && !(fromBlockSrc == 0 && toBlockSrc == 0)) { + JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet); + } + // eth_getLogs enforces the block range at construction time. eth_newFilter creates the // wrapper with checkBlockRange=false (no creation-time gate); eth_getFilterLogs re-runs this // check against the current head before scanning so the cap cannot be bypassed. diff --git a/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java new file mode 100644 index 00000000000..ec2703331af --- /dev/null +++ b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java @@ -0,0 +1,48 @@ +package org.tron.core.utils; + +import org.tron.core.vm.program.Program; +import org.tron.protos.Protocol.Transaction.Result.contractResult; + +/** + * Maps a TVM execution exception to its {@code contractResult} code for constant-call + * responses. Deliberately independent of RuntimeImpl's consensus-path classification. + */ +public class ResultCodeUtil { + + public static contractResult resolve(RuntimeException exception) { + if (exception instanceof Program.IllegalOperationException) { + return contractResult.ILLEGAL_OPERATION; + } + if (exception instanceof Program.OutOfEnergyException) { + return contractResult.OUT_OF_ENERGY; + } + if (exception instanceof Program.BadJumpDestinationException) { + return contractResult.BAD_JUMP_DESTINATION; + } + if (exception instanceof Program.OutOfTimeException) { + return contractResult.OUT_OF_TIME; + } + if (exception instanceof Program.OutOfMemoryException) { + return contractResult.OUT_OF_MEMORY; + } + if (exception instanceof Program.PrecompiledContractException) { + return contractResult.PRECOMPILED_CONTRACT; + } + if (exception instanceof Program.StackTooSmallException) { + return contractResult.STACK_TOO_SMALL; + } + if (exception instanceof Program.StackTooLargeException) { + return contractResult.STACK_TOO_LARGE; + } + if (exception instanceof Program.JVMStackOverFlowException) { + return contractResult.JVM_STACK_OVER_FLOW; + } + if (exception instanceof Program.TransferException) { + return contractResult.TRANSFER_FAILED; + } + if (exception instanceof Program.InvalidCodeException) { + return contractResult.INVALID_CODE; + } + return contractResult.UNKNOWN; + } +} diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index 3a13c7d5606..0edf429e0b5 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java @@ -59,6 +59,49 @@ public void before() { transactionStore.put(transactionId, transactionCapsule); } + @Test + public void getLowestBlockNum() { + Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong()); + } + + @Test + public void getLowestBlockNumPicksMinimumKey() { + transactionRetStore.put(ByteArray.fromLong(7), transactionRetCapsule); + transactionRetStore.put(ByteArray.fromLong(3), transactionRetCapsule); + try { + Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong()); + transactionRetStore.delete(blockNum); + Assert.assertEquals(3L, transactionRetStore.getLowestBlockNum().getAsLong()); + } finally { + transactionRetStore.delete(ByteArray.fromLong(3)); + transactionRetStore.delete(ByteArray.fromLong(7)); + } + } + + @Test + public void getLowestBlockNumOnEmptyStore() { + transactionRetStore.delete(blockNum); + Assert.assertFalse(transactionRetStore.getLowestBlockNum().isPresent()); + } + + @Test + public void probeLowestReceiptBlockNumReadsStore() { + // the probe runs after checkpoint recovery, so it must reflect whatever the store holds + // at call time: the first key while present, the next block once the store is empty + chainBaseManager.probeLowestReceiptBlockNum(); + Assert.assertEquals(1L, chainBaseManager.getLowestReceiptBlockNum()); + + // head must be non-zero, otherwise head + 1 collides with the first key asserted above + transactionRetStore.delete(blockNum); + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(5); + try { + chainBaseManager.probeLowestReceiptBlockNum(); + Assert.assertEquals(6L, chainBaseManager.getLowestReceiptBlockNum()); + } finally { + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(0); + } + } + @Test public void get() throws BadItemException { TransactionInfoCapsule resultCapsule = transactionRetStore.getTransactionInfo(transactionId); @@ -78,7 +121,12 @@ public void put() { Assert.assertNull("put transaction info error", transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); transactionRetStore.put(transactionInfoCapsule.getId(), transactionRetCapsule); - Assert.assertNotNull("get transaction info error", - transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); + try { + Assert.assertNotNull("get transaction info error", + transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); + } finally { + // a 32-byte key left behind breaks getNext's fixed-length key comparison + transactionRetStore.delete(transactionInfoCapsule.getId()); + } } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java index 33835c482fe..8f444806b55 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java @@ -11,7 +11,6 @@ import org.tron.common.logsfilter.capsule.LogsFilterCapsule; import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; -import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.FilterResult; @@ -45,7 +44,7 @@ private TransactionInfo buildTxInfoWithLog(byte[] address) { * Events dispatched to a matching filter in the serial (<=10000 entries) path. */ @Test - public void testMatchingFilter_receivesLogElements() throws JsonRpcInvalidParamsException { + public void testMatchingFilter_receivesLogElements() throws Exception { FilterRequest fr = new FilterRequest(); LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null); jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult); @@ -64,7 +63,7 @@ public void testMatchingFilter_receivesLogElements() throws JsonRpcInvalidParams * Filter with fromBlock=100 does not receive a capsule whose blockNumber is 50. */ @Test - public void testBlockNumberBelowRange_noResult() throws JsonRpcInvalidParamsException { + public void testBlockNumberBelowRange_noResult() throws Exception { FilterRequest fr = new FilterRequest(); // currentMaxBlockNum=100 → fromBlock=100, toBlock=MAX_VALUE LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null); @@ -110,7 +109,7 @@ public void testExpiredFilter_removedFromMap() throws Exception { * A solidified capsule is routed only to the solidity map; the full-node map is untouched. */ @Test - public void testSolidifiedCapsule_routedToSolidityMap() throws JsonRpcInvalidParamsException { + public void testSolidifiedCapsule_routedToSolidityMap() throws Exception { FilterRequest fr = new FilterRequest(); LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null); jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter); @@ -133,7 +132,7 @@ public void testSolidifiedCapsule_routedToSolidityMap() throws JsonRpcInvalidPar * A non-solidified capsule is routed only to the full-node map. */ @Test - public void testNonSolidifiedCapsule_routedToFullMap() throws JsonRpcInvalidParamsException { + public void testNonSolidifiedCapsule_routedToFullMap() throws Exception { FilterRequest fr = new FilterRequest(); LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null); jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter); @@ -156,7 +155,7 @@ public void testNonSolidifiedCapsule_routedToFullMap() throws JsonRpcInvalidPara * Both filters in the map receive events when both match. */ @Test - public void testMultipleMatchingFilters_bothReceiveEvents() throws JsonRpcInvalidParamsException { + public void testMultipleMatchingFilters_bothReceiveEvents() throws Exception { FilterRequest fr = new FilterRequest(); LogFilterAndResult filter1 = new LogFilterAndResult(fr, 100L, null); LogFilterAndResult filter2 = new LogFilterAndResult(fr, 100L, null); @@ -178,7 +177,7 @@ public void testMultipleMatchingFilters_bothReceiveEvents() throws JsonRpcInvali * An empty txInfoList produces no results. */ @Test - public void testEmptyTxInfoList_noResult() throws JsonRpcInvalidParamsException { + public void testEmptyTxInfoList_noResult() throws Exception { FilterRequest fr = new FilterRequest(); LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null); jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java index 2ab455fa580..6562f5b5c4f 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -17,11 +17,14 @@ import org.tron.core.Wallet; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.vm.program.Program; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; public class JsonRpcCallAndEstimateGasTest { @@ -55,76 +58,91 @@ public void tearDown() throws Exception { public void testGetCallAppendsRevertReason() throws Exception { byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); - mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT, + REVERT_MSG, EstimatePath.CONSTANT_CALL); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.getCall(newCallArgs(), "latest")); Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData()); } @Test public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception { - byte[] panicData = ByteArray.fromHexString("4e487b71" - + "0000000000000000000000000000000000000000000000000000000000000001"); + String panicHex = "4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"; + byte[] panicData = ByteArray.fromHexString(panicHex); - mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL); + mockRpc = newRpcWithMockedFailedCall(panicData, contractResult.REVERT, + REVERT_MSG, EstimatePath.CONSTANT_CALL); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.getCall(newCallArgs(), "latest")); Assert.assertEquals(REVERT_MSG, e.getMessage()); + Assert.assertEquals("0x" + panicHex, e.getData()); } @Test public void testGetCallSkipsRevertReasonForShortData() throws Exception { - mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL); + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, contractResult.REVERT, + REVERT_MSG, EstimatePath.CONSTANT_CALL); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.getCall(newCallArgs(), "latest")); Assert.assertEquals(REVERT_MSG, e.getMessage()); + Assert.assertEquals("0x010203", e.getData()); } @Test public void testEstimateGasAppendsRevertReason() throws Exception { byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); - mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT, + REVERT_MSG, EstimatePath.CONSTANT_CALL); CommonParameter.getInstance().setEstimateEnergy(false); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.estimateGas(newCallArgs())); Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData()); } @Test public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception { - mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL); + mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.REVERT, + REVERT_MSG, EstimatePath.CONSTANT_CALL); CommonParameter.getInstance().setEstimateEnergy(false); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.estimateGas(newCallArgs())); Assert.assertEquals(REVERT_MSG, e.getMessage()); + Assert.assertEquals("0x", e.getData()); } @Test public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception { byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); - mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY); + mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT, + REVERT_MSG, EstimatePath.ESTIMATE_ENERGY); CommonParameter.getInstance().setEstimateEnergy(true); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.estimateGas(newCallArgs())); Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData()); } @Test public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception { - mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY); + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, contractResult.REVERT, + REVERT_MSG, EstimatePath.ESTIMATE_ENERGY); CommonParameter.getInstance().setEstimateEnergy(true); - JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class, () -> mockRpc.estimateGas(newCallArgs())); Assert.assertEquals(REVERT_MSG, e.getMessage()); + Assert.assertEquals("0x010203", e.getData()); } @Test @@ -140,6 +158,68 @@ public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exc Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); } + @Test + public void testGetCallNonRevertFailureIsNotExecutionReverted() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetCallNonRevertFailureAttachesReturnData() throws Exception { + byte[] resData = ByteArray.fromHexString("deadbeef00"); + mockRpc = newRpcWithMockedFailedCall(resData, contractResult.DEFAULT, + "Unknown failure", EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Unknown failure", e.getMessage()); + Assert.assertEquals("0xdeadbeef00", e.getData()); + } + + @Test + public void testGetCallThrownVmExceptionIsInternalError() throws Exception { + // for a plain eth_call, Wallet rethrows every non-revert VM failure instead of + // returning a FAILED result, so it must surface as -32000 without data + mockRpc = newRpcWithThrowingCall(new Program.OutOfEnergyException("Not enough energy")); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Not enough energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testEstimateGasWithEstimateEnergyNonRevertFailureIsNotExecutionReverted() + throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testEstimateGasWithEstimateEnergyNonRevertFailureAttachesReturnData() + throws Exception { + byte[] resData = ByteArray.fromHexString("deadbeef00"); + mockRpc = newRpcWithMockedFailedCall(resData, contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertEquals("0xdeadbeef00", e.getData()); + } + @Test public void testGetCallReturnsConstantResult() throws Exception { byte[] part1 = ByteArray.fromHexString("deadbeef"); @@ -173,8 +253,8 @@ private static CallArguments newCallArgs() { return args; } - private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path) - throws Exception { + private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, + contractResult contractRet, String message, EstimatePath path) throws Exception { Wallet mockWallet = mock(Wallet.class); Manager mockManager = mock(Manager.class); NodeInfoService mockNodeInfo = mock(NodeInfoService.class); @@ -183,6 +263,12 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + Protocol.Transaction failedTransaction = Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED) + .setContractRet(contractRet)) + .build(); + if (path == EstimatePath.ESTIMATE_ENERGY) { when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) .thenAnswer(invocation -> { @@ -190,12 +276,9 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima Return.Builder retBuilder = invocation.getArgument(3); EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); extBuilder.addConstantResult(ByteString.copyFrom(resData)); - retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + retBuilder.setMessage(ByteString.copyFromUtf8(message)); estimateBuilder.setResult(retBuilder); - return Protocol.Transaction.newBuilder() - .addRet(Protocol.Transaction.Result.newBuilder() - .setRet(Protocol.Transaction.Result.code.FAILED)) - .build(); + return failedTransaction; }); } else { when(mockWallet.triggerConstantContract(any(), any(), any(), any())) @@ -203,11 +286,8 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima TransactionExtention.Builder extBuilder = invocation.getArgument(2); Return.Builder retBuilder = invocation.getArgument(3); extBuilder.addConstantResult(ByteString.copyFrom(resData)); - retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); - return Protocol.Transaction.newBuilder() - .addRet(Protocol.Transaction.Result.newBuilder() - .setRet(Protocol.Transaction.Result.code.FAILED)) - .build(); + retBuilder.setMessage(ByteString.copyFromUtf8(message)); + return failedTransaction; }); } @@ -216,6 +296,23 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima return rpc; } + private static TronJsonRpcImpl newRpcWithThrowingCall(RuntimeException vmException) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenThrow(vmException); + + TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet); + rpc.setManager(mockManager); + return rpc; + } + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) throws Exception { Wallet mockWallet = mock(Wallet.class); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java new file mode 100644 index 00000000000..d10ec796c0c --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -0,0 +1,418 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Strings; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.TransactionInfoList; +import org.tron.core.Wallet; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; + +public class JsonRpcPrunedHistoryTest { + + private static final long LOWEST_BLOCK_NUM = 100L; + private static final long RECEIPT_FLOOR_BLOCK_NUM = 150L; + private static final long HEAD_BLOCK_NUM = 200L; + private static final String PRUNED_MESSAGE = "Pruned history unavailable"; + private static final String BODY_FLOOR_HEX = "0x64"; + private static final String BELOW_CUTOFF_HEX = "0x10"; + private static final String AT_CUTOFF_HEX = "0x64"; + private static final long IN_RECEIPT_GAP_NUM = 112L; + private static final String IN_RECEIPT_GAP_HEX = "0x70"; + private static final String RECEIPT_FLOOR_HEX = "0x96"; + + private TronJsonRpcImpl rpc; + + @After + public void tearDown() throws IOException { + if (rpc != null) { + rpc.close(); + rpc = null; + } + } + + private static Block newBlock(long number, int transactionCount) { + Block.Builder builder = Block.newBuilder().setBlockHeader(BlockHeader.newBuilder() + .setRawData(BlockHeader.raw.newBuilder().setNumber(number))); + for (int i = 0; i < transactionCount; i++) { + builder.addTransactions(Transaction.newBuilder()); + } + return builder.build(); + } + + private static Wallet newMockWallet(boolean liteNode) { + Wallet wallet = mock(Wallet.class); + when(wallet.isLiteNode()).thenReturn(liteNode); + when(wallet.getLowestBlockNum()).thenReturn(liteNode ? LOWEST_BLOCK_NUM : 0L); + when(wallet.getLowestReceiptBlockNum()) + .thenReturn(liteNode ? RECEIPT_FLOOR_BLOCK_NUM : 0L); + when(wallet.getCursor()).thenReturn(Chainbase.Cursor.HEAD); + when(wallet.getNowBlock()).thenReturn(newBlock(HEAD_BLOCK_NUM, 0)); + // a LiteNode snapshot copies genesis explicitly, so block 0 stays retrievable below the cutoff + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 0)); + return wallet; + } + + private TronJsonRpcImpl newRpc(boolean liteNode) { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newMockWallet(liteNode)); + return rpc; + } + + private static Wallet newHistoryOffMockWallet() { + return newHistoryOffMockWallet(true); + } + + private static Wallet newHistoryOffMockWallet(boolean liteNode) { + Wallet wallet = newMockWallet(liteNode); + when(wallet.getLowestReceiptBlockNum()).thenReturn(Long.MAX_VALUE); + return wallet; + } + + @Test + public void testParseBlockTagEarliestOnLiteNode() throws Exception { + Assert.assertEquals(RECEIPT_FLOOR_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(true))); + } + + @Test + public void testParseBlockTagEarliestOnFullNode() throws Exception { + Assert.assertEquals(0L, JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(false))); + } + + @Test + public void testGetBlockByNumberBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + // data carries the same height "earliest" resolves to, not the endpoint's own floor + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockByNumberBelowCutoffWithHistoryOffReportsBodyFloor() { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockByNumberAtCutoffPasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber(AT_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockByNumberEarliestOnLiteNodePasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetBlockByNumberOnFullNodePasses() throws Exception { + Assert.assertNull(newRpc(false).ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockTransactionCountGenesisOnLiteNodePasses() throws Exception { + // genesis is retained below the cutoff, so a single-block lookup must not return 4444 + Assert.assertEquals("0x0", newRpc(true).ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetBlockTransactionCountGenesisWithTransactionsOnLiteNode() throws Exception { + // Genesis carries initial-allocation transactions in its body but never has transactionInfo + // (initGenesis writes only blockStore/blockIndexStore, no processBlock). The count endpoint + // reads the body, so it must return the real count, not 4444. + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 2)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertEquals("0x2", rpc.ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexGenesisOnLiteNodePasses() throws Exception { + // index beyond the (empty) genesis body yields null, not 4444 + Assert.assertNull(newRpc(true).getTransactionByBlockNumberAndIndex("0x0", "0x0")); + } + + @Test + public void testFutureBlockOnLiteNodeReturnsNullNotPruned() throws Exception { + // above the cutoff and simply not produced yet: null, never 4444 + Assert.assertNull(newRpc(true).ethGetBlockByNumber("0x7fffffff", false)); + } + + @Test + public void testGetBlockTransactionCountBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockTransactionCountByNumber(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0")); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsBelowCutoffWithHistoryOffReportsBodyFloor() { + // with receipts never persisted, "earliest" falls back to the body floor and so does data + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetLogsFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testNewFilterFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.newFilter(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testNewFilterEarliestOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("earliest", "latest", null, null, null); + + String filterId = newRpc(true).newFilter(fr); + + Assert.assertNotNull(filterId); + Assert.assertTrue(filterId.startsWith("0x")); + } + + @Test + public void testLogFilterGenesisOnlyRangeOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("0x0", "0x0", null, null, null); + + LogFilterWrapper wrapper = + new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + Assert.assertEquals(0L, wrapper.getToBlock()); + } + + @Test + public void testLogFilterGenesisBlockHashOnLiteNodePasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockById(org.mockito.ArgumentMatchers.any())) + .thenReturn(newBlock(0L, 0)); + FilterRequest fr = new FilterRequest(null, null, null, null, + "0x" + Strings.repeat("00", 32)); + + LogFilterWrapper wrapper = new LogFilterWrapper(fr, HEAD_BLOCK_NUM, wallet, false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + } + + @Test + public void testLogFilterEarliestWithLowToBlockIsInvalidRange() { + FilterRequest fr = new FilterRequest("earliest", "0x5", null, null, null); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false)); + } + + @Test + public void testGetLogsInReceiptGapReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest(IN_RECEIPT_GAP_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsInReceiptGapReturns4444() { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { + // the floor is checked before the body is read, so an empty block is not special-cased + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsAtReceiptFloorPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(TransactionInfoList.getDefaultInstance()); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + } + + @Test + public void testGetBlockTransactionCountInReceiptGapPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertEquals("0x1", rpc.ethGetBlockTransactionCountByNumber(IN_RECEIPT_GAP_HEX)); + } + + @Test + public void testParseBlockTagEarliestWithHistoryOffFallsBackToBodyFloor() throws Exception { + Assert.assertEquals(LOWEST_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newHistoryOffMockWallet())); + } + + @Test + public void testGetBlockByNumberEarliestWithHistoryOffPasses() throws Exception { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + + Assert.assertNull(rpc.ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetLogsWithHistoryOffReturns4444() { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + FilterRequest fr = new FilterRequest(RECEIPT_FLOOR_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() { + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> JsonRpcApiUtil.checkPrunedReceiptHistory(Long.MAX_VALUE, newHistoryOffMockWallet())); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetLogsOnFullNodeWithHistoryOffReturns4444() { + // receipt persistence is a per-node switch, so the LiteNode gate must not shield a FullNode + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false)); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsOnFullNodeWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(false); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockByNumberOnFullNodeWithHistoryOffPasses() throws Exception { + // bodies are unaffected by the receipt switch + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false)); + + Assert.assertNull(rpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } +} 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 49f875f3823..06f57750fbe 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -20,6 +20,7 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Commons; +import org.tron.core.exception.jsonrpc.JsonRpcException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.jsonrpc.JsonRpcApiUtil; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; @@ -408,7 +409,7 @@ public void testGetConditions() { Assert.assertArrayEquals(conditions[2][4], getBloomIndex("0x00000000000000000000000056178a0d5f301baf6cf3e1cd53d9863437345bf9")); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } } @@ -448,7 +449,7 @@ public void testGetConditionWithHashCollision() { Assert.assertArrayEquals(conditions[0][1], getBloomIndex("0x3038114c1a1e72c5bfa8b003bc3650ad2ba254a0")); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } } 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 e8d14ace060..efdcfc669d2 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -46,6 +46,7 @@ import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.capsule.utils.BlockUtil; import org.tron.core.config.args.Args; +import org.tron.core.exception.jsonrpc.JsonRpcException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.NodeInfoService; @@ -825,7 +826,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, null, null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -835,7 +836,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest("0x14", null, null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -845,7 +846,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest("0x78", null, null, null, null), 100, null, false); Assert.assertEquals(120, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -855,7 +856,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, "0x14", null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(20, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -865,7 +866,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, "0x78", null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(120, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -875,7 +876,7 @@ public void testLogFilterWrapper() { null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(120, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException fromToEx = @@ -887,10 +888,10 @@ public void testLogFilterWrapper() { //fromBlock or toBlock is not hex num try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper(new FilterRequest("earliest", null, - null, null, null), 100, null, false); + null, null, null), 100, wallet, false); Assert.assertEquals(0, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -898,7 +899,7 @@ public void testLogFilterWrapper() { null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException pendingFilterEx = Assert.assertThrows( @@ -911,7 +912,7 @@ public void testLogFilterWrapper() { null, null, null), 100, wallet, false); Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException testSyntaxEx = Assert.assertThrows( @@ -924,7 +925,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -939,7 +940,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -955,7 +956,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x64", "latest", null, null, null), 5_000, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -997,13 +998,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("latest", "latest", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("latest", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1012,13 +1013,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1026,13 +1027,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1040,13 +1041,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1101,7 +1102,7 @@ public void testMaxSubTopics() { tronJsonRpc.getLogs(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), e.getMessage()); @@ -1113,7 +1114,7 @@ public void testMaxSubTopics() { tronJsonRpc.newFilter(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), e.getMessage()); @@ -1126,7 +1127,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1134,7 +1135,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -1148,7 +1149,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -1167,7 +1168,7 @@ public void testMethodBlockRange() { tronJsonRpc.getLogs(new FilterRequest("0x0", "0x1f40", null, null, null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, e.getMessage()); @@ -1375,28 +1376,28 @@ public void testGetBlockReceipts() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), transactionReceipt1.getLogs()[0].getBlockTimestamp()); } - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("earliest"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("latest"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("finalized"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1415,7 +1416,7 @@ public void testGetBlockReceipts() { try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1429,7 +1430,7 @@ public void testGetBlockReceipts() { Assert.assertFalse(transactionReceiptList.isEmpty()); Assert.assertEquals(JSON.toJSONString(transactionReceiptList), JSON.toJSONString(transactionReceiptList2)); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java index 77f869fd5a8..5ae31e57110 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java @@ -76,7 +76,7 @@ private Manager buildMockManager(long block1, TransactionInfoList txList1, } private LogMatch buildLogMatch(List blockNums, Manager manager) - throws JsonRpcInvalidParamsException { + throws Exception { FilterRequest fr = new FilterRequest(); // match-all filter LogFilterWrapper wrapper = new LogFilterWrapper(fr, 0L, null, false); return new LogMatch(wrapper, blockNums, manager); @@ -85,8 +85,7 @@ private LogMatch buildLogMatch(List blockNums, Manager manager) /** Under the limit: all logs returned without exception. */ @Test public void testUnderLimit_returnsAllResults() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { int logCount = MAX_RESULT / 2; // 5000, well under limit Manager manager = buildMockManager(100L, buildTxList(logCount)); LogMatch logMatch = buildLogMatch(Collections.singletonList(100L), manager); @@ -101,8 +100,7 @@ public void testUnderLimit_returnsAllResults() */ @Test public void testAtExactLimit_succeeds() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 1 log → total == MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -119,7 +117,7 @@ public void testAtExactLimit_succeeds() */ @Test public void testExceedsLimit_throws() - throws ItemNotFoundException, JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 2 logs → 9999 + 2 = 10001 > MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -132,8 +130,7 @@ public void testExceedsLimit_throws() /** A block with no matching logs is skipped without incrementing the result count. */ @Test public void testEmptyBlockSkipped() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: no logs (empty txInfoList → skipped), block 2: 3 logs Manager manager = mock(Manager.class); ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java index 39bcc30e278..3b7c796fbb0 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java @@ -15,6 +15,7 @@ import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.config.args.Args; import org.tron.core.exception.EventBloomException; @@ -30,6 +31,9 @@ public class SectionBloomStoreTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + @Resource + private Wallet wallet; + private ExecutorService sectionExecutor; static { @@ -145,7 +149,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", ByteArray.toJsonHex(address1), null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -162,7 +166,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", addressList, null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -178,7 +182,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic1)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -194,7 +198,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -212,7 +216,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {topicList}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -239,7 +243,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {ByteArray.toJsonHex(topic1), ByteArray.toJsonHex(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java index d8e64308ab8..5fe007e27dc 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java @@ -11,6 +11,7 @@ import org.junit.Assert; import org.junit.Test; import org.tron.core.exception.jsonrpc.JsonRpcException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; @@ -22,6 +23,7 @@ public class JsonRpcErrorResolverTest { @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), @JsonRpcError(exception = JsonRpcException.class, code = -1) }) @@ -61,6 +63,16 @@ public void testResolveErrorWithTronException() throws Exception { Assert.assertEquals(message, error.message); Assert.assertEquals("{}", error.data); + message = "execution reverted"; + data = "0x"; + exception = new JsonRpcExecutionRevertedException(message, data); + error = resolver.resolveError(exception, method, arguments); + + Assert.assertNotNull(error); + Assert.assertEquals(3, error.code); + Assert.assertEquals(message, error.message); + Assert.assertEquals(data, error.data); + message = "JsonRpcException"; exception = new JsonRpcException(message, null); error = resolver.resolveError(exception, method, arguments); @@ -72,4 +84,28 @@ public void testResolveErrorWithTronException() throws Exception { } -} \ No newline at end of file + @Test + public void testAnnotationOrderNeverShadowsSubclassCodes() { + for (Method method : TronJsonRpc.class.getMethods()) { + JsonRpcErrors errors = method.getAnnotation(JsonRpcErrors.class); + if (errors == null) { + continue; + } + JsonRpcError[] entries = errors.value(); + for (int earlier = 0; earlier < entries.length; earlier++) { + for (int later = earlier + 1; later < entries.length; later++) { + Class earlierEx = entries[earlier].exception(); + Class laterEx = entries[later].exception(); + Assert.assertFalse(String.format( + "%s.%s: @JsonRpcError for %s (code %d) is unreachable — its superclass %s " + + "(code %d) is declared before it; move the subclass entry up", + TronJsonRpc.class.getSimpleName(), method.getName(), + laterEx.getSimpleName(), entries[later].code(), + earlierEx.getSimpleName(), entries[earlier].code()), + earlierEx != laterEx && earlierEx.isAssignableFrom(laterEx)); + } + } + } + } + +} diff --git a/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java new file mode 100644 index 00000000000..266562c5eea --- /dev/null +++ b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java @@ -0,0 +1,66 @@ +package org.tron.core.utils; + +import java.lang.reflect.Method; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.runtime.ProgramResult; +import org.tron.common.runtime.RuntimeImpl; +import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.Program.BadJumpDestinationException; +import org.tron.core.vm.program.Program.OutOfEnergyException; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.protos.Protocol.Transaction.Result.contractResult; + +public class ResultCodeUtilTest { + + @Test + public void testTypedExceptionsResolveToTheirCodes() { + Assert.assertEquals(contractResult.OUT_OF_ENERGY, + ResultCodeUtil.resolve(new OutOfEnergyException("out of energy"))); + Assert.assertEquals(contractResult.OUT_OF_TIME, + ResultCodeUtil.resolve(new OutOfTimeException("out of time"))); + Assert.assertEquals(contractResult.BAD_JUMP_DESTINATION, + ResultCodeUtil.resolve(new BadJumpDestinationException("bad jump"))); + } + + @Test + public void testUntypedExceptionResolvesToUnknown() { + Assert.assertEquals(contractResult.UNKNOWN, + ResultCodeUtil.resolve(new RuntimeException("untyped failure"))); + } + + @Test + public void testNullExceptionResolvesToUnknown() { + Assert.assertEquals(contractResult.UNKNOWN, ResultCodeUtil.resolve(null)); + } + + @Test + public void testStaysInSyncWithConsensusClassifier() throws Exception { + RuntimeException[] samples = { + new Program.IllegalOperationException("op"), + new Program.OutOfEnergyException("energy"), + new Program.BadJumpDestinationException("jump"), + new Program.OutOfTimeException("time"), + new Program.OutOfMemoryException("mem"), + new Program.PrecompiledContractException("pre"), + new Program.StackTooSmallException("small"), + new Program.JVMStackOverFlowException(), + new Program.TransferException("transfer"), + new Program.InvalidCodeException("code"), + new Program.StaticCallModificationException(), + new RuntimeException("untyped"), + }; + + Method consensus = RuntimeImpl.class.getDeclaredMethod("setResultCode", ProgramResult.class); + consensus.setAccessible(true); + RuntimeImpl runtime = new RuntimeImpl(); + + for (RuntimeException e : samples) { + ProgramResult result = new ProgramResult(); + result.setException(e); + consensus.invoke(runtime, result); + Assert.assertEquals("classifiers diverged for " + e.getClass().getSimpleName(), + result.getResultCode(), ResultCodeUtil.resolve(e)); + } + } +} From c3fccfc3b6541200330b310315e7fe393f9a6aa6 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 28 Sep 2026 10:21:01 +0800 Subject: [PATCH 2/7] fix(jsonrpc): drop data from 4444 and remove ResultCodeUtil - 4444 no longer carries the earliest available block in data, as geth - remove ResultCodeUtil; constant calls record only REVERT - test that a reverted constant call sets contractRet to REVERT --- .../java/org/tron/core/ChainBaseManager.java | 1 - .../tron/core/store/TransactionRetStore.java | 10 +-- .../JsonRpcPrunedHistoryException.java | 6 +- .../src/main/java/org/tron/core/Wallet.java | 3 - .../core/services/jsonrpc/JsonRpcApiUtil.java | 18 +---- .../services/jsonrpc/TronJsonRpcImpl.java | 5 +- .../org/tron/core/utils/ResultCodeUtil.java | 48 -------------- .../test/java/org/tron/core/WalletTest.java | 29 ++++++++ .../jsonrpc/JsonRpcPrunedHistoryTest.java | 41 ++++-------- .../tron/core/utils/ResultCodeUtilTest.java | 66 ------------------- 10 files changed, 53 insertions(+), 174 deletions(-) delete mode 100644 framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java delete mode 100644 framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 2fd05c8b73e..35fcd73bb6d 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -248,7 +248,6 @@ public class ChainBaseManager { // lowest block with receipts; above lowestBlockNum on a LiteNode @Getter - @Setter private long lowestReceiptBlockNum = -1; @Getter diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index 6b08c9dd69b..ec9ceb5fc7a 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -2,7 +2,6 @@ import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; -import java.util.Map; import java.util.Objects; import java.util.OptionalLong; import lombok.extern.slf4j.Slf4j; @@ -48,13 +47,8 @@ public void put(byte[] key, TransactionRetCapsule item) { * layers, {@code getNext} does not merge deletions correctly. */ public OptionalLong getLowestBlockNum() { - Map entries = revokingDB.getNext(ByteArray.fromLong(0), 1); - for (byte[] key : entries.keySet()) { - if (key.length == Long.BYTES) { - return OptionalLong.of(Longs.fromByteArray(key)); - } - } - return OptionalLong.empty(); + return revokingDB.getNext(ByteArray.fromLong(0), 1).keySet().stream() + .mapToLong(Longs::fromByteArray).findFirst(); } public TransactionInfoCapsule getTransactionInfo(byte[] key) throws BadItemException { diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java index d5cf4f0c887..259081b317b 100644 --- a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java @@ -1,9 +1,9 @@ package org.tron.core.exception.jsonrpc; /** - * Thrown when a request targets historical state that a LiteNode has pruned. - * Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized - * by the Ethereum Execution API (EIP-4444). + * Thrown when a request targets data the node does not have: history pruned on a LiteNode, + * or receipts and logs on a node that does not persist them. Maps to JSON-RPC error code 4444 + * "Pruned history unavailable", as standardized by the Ethereum Execution API (EIP-4444). */ public class JsonRpcPrunedHistoryException extends JsonRpcException { diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index f1d24740269..51663abd52b 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -201,7 +201,6 @@ import org.tron.core.store.StoreFactory; import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; -import org.tron.core.utils.ResultCodeUtil; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; @@ -3200,8 +3199,6 @@ public Transaction callConstantContract(TransactionCapsule trxCap, ret.setStatus(0, code.SUCESS); if (StringUtils.isNoneEmpty(result.getRuntimeError())) { ret.setStatus(0, code.FAILED); - // same failure classification as executed transactions - ret.setResultCode(ResultCodeUtil.resolve(result.getException())); retBuilder .setMessage(ByteString.copyFromUtf8(result.getRuntimeError())) .build(); 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 ce183a81f45..17dc42f30e6 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 @@ -704,7 +704,7 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) /** * The lowest block for which everything the node persists is available; what "earliest" - * resolves to and what a 4444 error carries in {@code data}. On a LiteNode this is the + * resolves to. On a LiteNode this is the * receipt floor when receipts are persisted, otherwise the body floor (receipt endpoints * answer 4444 on such a node regardless of this value). On a FullNode it is genesis. */ @@ -723,7 +723,7 @@ public static long earliestAvailableBlock(Wallet wallet) { public static void checkPrunedHistory(long blockNum, Wallet wallet) throws JsonRpcPrunedHistoryException { if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { - throw prunedHistory(earliestAvailableBlock(wallet)); + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); } } @@ -736,21 +736,9 @@ public static void checkPrunedHistory(long blockNum, Wallet wallet) public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) throws JsonRpcPrunedHistoryException { long receiptFloor = wallet.getLowestReceiptBlockNum(); - if (receiptFloor == Long.MAX_VALUE) { + if (receiptFloor == Long.MAX_VALUE || wallet.isLiteNode() && blockNum < receiptFloor) { throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); } - if (wallet.isLiteNode() && blockNum < receiptFloor) { - throw prunedHistory(receiptFloor); - } - } - - /** - * The Execution API fixes the message verbatim; the earliest available block travels in - * {@code data} so a client can pick a fallback node from it. - */ - private static JsonRpcPrunedHistoryException prunedHistory(long earliestAvailable) { - return new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR, - "0x" + Long.toHexString(earliestAvailable)); } /** 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 c2d933b277c..c99c8cbec37 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 @@ -934,6 +934,7 @@ private TransactionContext findTransactionContext(TransactionInfoList infoList, * @return List of TransactionReceipt objects for all transactions in the block, * null if block not found * @throws JsonRpcInvalidParamsException if the parameter format is invalid + * @throws JsonRpcPrunedHistoryException if the node does not have the block's receipts * @throws JsonRpcInternalException if there's an internal error */ @Override @@ -950,14 +951,13 @@ public List getBlockReceipts(String blockNumOrHashOrTag) block = getBlockByNumOrTag(blockNumOrHashOrTag); } - // block receipts not available: block is genesis, not produced yet, or pruned in light node + // block receipts not available: block is genesis, not produced yet, or unknown hash if (block == null || block.getBlockHeader().getRawData().getNumber() == 0) { return null; } BlockCapsule blockCapsule = new BlockCapsule(block); long blockNum = blockCapsule.getNum(); - int transactionSizeInBlock = blockCapsule.getTransactions().size(); // below the receipt floor the body exists but the receipts do not — 4444, not -32000 JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet); TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum); @@ -966,6 +966,7 @@ public List getBlockReceipts(String blockNumOrHashOrTag) long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp()); // Validate transaction list size consistency + int transactionSizeInBlock = blockCapsule.getTransactions().size(); if (transactionSizeInBlock != transactionInfoList.getTransactionInfoCount()) { throw new JsonRpcInternalException( String.format("TransactionList size mismatch: " diff --git a/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java deleted file mode 100644 index ec2703331af..00000000000 --- a/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.tron.core.utils; - -import org.tron.core.vm.program.Program; -import org.tron.protos.Protocol.Transaction.Result.contractResult; - -/** - * Maps a TVM execution exception to its {@code contractResult} code for constant-call - * responses. Deliberately independent of RuntimeImpl's consensus-path classification. - */ -public class ResultCodeUtil { - - public static contractResult resolve(RuntimeException exception) { - if (exception instanceof Program.IllegalOperationException) { - return contractResult.ILLEGAL_OPERATION; - } - if (exception instanceof Program.OutOfEnergyException) { - return contractResult.OUT_OF_ENERGY; - } - if (exception instanceof Program.BadJumpDestinationException) { - return contractResult.BAD_JUMP_DESTINATION; - } - if (exception instanceof Program.OutOfTimeException) { - return contractResult.OUT_OF_TIME; - } - if (exception instanceof Program.OutOfMemoryException) { - return contractResult.OUT_OF_MEMORY; - } - if (exception instanceof Program.PrecompiledContractException) { - return contractResult.PRECOMPILED_CONTRACT; - } - if (exception instanceof Program.StackTooSmallException) { - return contractResult.STACK_TOO_SMALL; - } - if (exception instanceof Program.StackTooLargeException) { - return contractResult.STACK_TOO_LARGE; - } - if (exception instanceof Program.JVMStackOverFlowException) { - return contractResult.JVM_STACK_OVER_FLOW; - } - if (exception instanceof Program.TransferException) { - return contractResult.TRANSFER_FAILED; - } - if (exception instanceof Program.InvalidCodeException) { - return contractResult.INVALID_CODE; - } - return contractResult.UNKNOWN; - } -} diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index 7215a287912..be46cfc9265 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -1386,6 +1386,35 @@ public void testEstimateEnergyOutOfTime() { contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder)); } + @Test + @SneakyThrows + public void testTriggerConstantRevertSetsContractRet() { + String contractAddress = "0x1A622D84ed49f01045f5f1a5AfcEb9c57e9cC3cd"; + + SmartContractOuterClass.SmartContract smartContract = + SmartContractOuterClass.SmartContract.newBuilder().build(); + ContractCapsule capsule = new ContractCapsule(smartContract); + dbManager.getContractStore().put(ByteArray.fromHexString(contractAddress), capsule); + + // PUSH1 0 PUSH1 0 REVERT + CodeCapsule codeCapsule = new CodeCapsule(ByteArray.fromHexString("60006000fd")); + dbManager.getCodeStore().put(ByteArray.fromHexString(contractAddress), codeCapsule); + + SmartContractOuterClass.TriggerSmartContract contract = + SmartContractOuterClass.TriggerSmartContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS))) + .setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(contractAddress))) + .build(); + TransactionCapsule trxCap = wallet.createTransactionCapsule(contract, + ContractType.TriggerSmartContract); + + Transaction tx = wallet.triggerConstantContract(contract, trxCap, + GrpcAPI.TransactionExtention.newBuilder(), GrpcAPI.Return.newBuilder()); + Assert.assertEquals(Transaction.Result.code.FAILED, tx.getRet(0).getRet()); + Assert.assertEquals(Transaction.Result.contractResult.REVERT, + tx.getRet(0).getContractRet()); + } + @Test public void testListNodes() { try { diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java index d10ec796c0c..d7c70f55825 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -29,7 +29,6 @@ public class JsonRpcPrunedHistoryTest { private static final long RECEIPT_FLOOR_BLOCK_NUM = 150L; private static final long HEAD_BLOCK_NUM = 200L; private static final String PRUNED_MESSAGE = "Pruned history unavailable"; - private static final String BODY_FLOOR_HEX = "0x64"; private static final String BELOW_CUTOFF_HEX = "0x10"; private static final String AT_CUTOFF_HEX = "0x64"; private static final long IN_RECEIPT_GAP_NUM = 112L; @@ -101,18 +100,7 @@ public void testGetBlockByNumberBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - // data carries the same height "earliest" resolves to, not the endpoint's own floor - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); - } - - @Test - public void testGetBlockByNumberBelowCutoffWithHistoryOffReportsBodyFloor() { - rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); - - JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, - () -> rpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); - Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -167,7 +155,7 @@ public void testGetBlockTransactionCountBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.ethGetBlockTransactionCountByNumber(BELOW_CUTOFF_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -177,7 +165,7 @@ public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0")); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -187,18 +175,15 @@ public void testGetBlockReceiptsBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test - public void testGetBlockReceiptsBelowCutoffWithHistoryOffReportsBodyFloor() { - // with receipts never persisted, "earliest" falls back to the body floor and so does data + public void testGetBlockReceiptsFutureBlockWithHistoryOffReturnsNull() throws Exception { + // a block that does not exist yet is not pruned history rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); - JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, - () -> rpc.getBlockReceipts(BELOW_CUTOFF_HEX)); - Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + Assert.assertNull(rpc.getBlockReceipts("0x7fffffff")); } @Test @@ -209,7 +194,7 @@ public void testGetLogsFromBlockBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.getLogs(fr)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -220,7 +205,7 @@ public void testNewFilterFromBlockBelowCutoffReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.newFilter(fr)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -273,7 +258,7 @@ public void testGetLogsInReceiptGapReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> liteRpc.getLogs(fr)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test @@ -286,12 +271,12 @@ public void testGetBlockReceiptsInReceiptGapReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { - // the floor is checked before the body is read, so an empty block is not special-cased + // the floor check ignores the transaction count, so an empty block is not special-cased Wallet wallet = newMockWallet(true); when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0)); @@ -300,7 +285,7 @@ public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); - Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + Assert.assertNull(e.getData()); } @Test diff --git a/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java deleted file mode 100644 index 266562c5eea..00000000000 --- a/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.tron.core.utils; - -import java.lang.reflect.Method; -import org.junit.Assert; -import org.junit.Test; -import org.tron.common.runtime.ProgramResult; -import org.tron.common.runtime.RuntimeImpl; -import org.tron.core.vm.program.Program; -import org.tron.core.vm.program.Program.BadJumpDestinationException; -import org.tron.core.vm.program.Program.OutOfEnergyException; -import org.tron.core.vm.program.Program.OutOfTimeException; -import org.tron.protos.Protocol.Transaction.Result.contractResult; - -public class ResultCodeUtilTest { - - @Test - public void testTypedExceptionsResolveToTheirCodes() { - Assert.assertEquals(contractResult.OUT_OF_ENERGY, - ResultCodeUtil.resolve(new OutOfEnergyException("out of energy"))); - Assert.assertEquals(contractResult.OUT_OF_TIME, - ResultCodeUtil.resolve(new OutOfTimeException("out of time"))); - Assert.assertEquals(contractResult.BAD_JUMP_DESTINATION, - ResultCodeUtil.resolve(new BadJumpDestinationException("bad jump"))); - } - - @Test - public void testUntypedExceptionResolvesToUnknown() { - Assert.assertEquals(contractResult.UNKNOWN, - ResultCodeUtil.resolve(new RuntimeException("untyped failure"))); - } - - @Test - public void testNullExceptionResolvesToUnknown() { - Assert.assertEquals(contractResult.UNKNOWN, ResultCodeUtil.resolve(null)); - } - - @Test - public void testStaysInSyncWithConsensusClassifier() throws Exception { - RuntimeException[] samples = { - new Program.IllegalOperationException("op"), - new Program.OutOfEnergyException("energy"), - new Program.BadJumpDestinationException("jump"), - new Program.OutOfTimeException("time"), - new Program.OutOfMemoryException("mem"), - new Program.PrecompiledContractException("pre"), - new Program.StackTooSmallException("small"), - new Program.JVMStackOverFlowException(), - new Program.TransferException("transfer"), - new Program.InvalidCodeException("code"), - new Program.StaticCallModificationException(), - new RuntimeException("untyped"), - }; - - Method consensus = RuntimeImpl.class.getDeclaredMethod("setResultCode", ProgramResult.class); - consensus.setAccessible(true); - RuntimeImpl runtime = new RuntimeImpl(); - - for (RuntimeException e : samples) { - ProgramResult result = new ProgramResult(); - result.setException(e); - consensus.invoke(runtime, result); - Assert.assertEquals("classifiers diverged for " + e.getClass().getSimpleName(), - result.getResultCode(), ResultCodeUtil.resolve(e)); - } - } -} From daf0008aa4d45919bc9340012aeaa4cc51f571a6 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 28 Sep 2026 17:31:47 +0800 Subject: [PATCH 3/7] refactor(jsonrpc): rename lowestReceiptBlockNum to lowestBlockNumOfReceiptStore --- .../java/org/tron/core/ChainBaseManager.java | 13 +++--- .../tron/core/store/TransactionRetStore.java | 2 +- .../src/main/java/org/tron/core/Wallet.java | 4 +- .../main/java/org/tron/core/db/Manager.java | 2 +- .../core/services/jsonrpc/JsonRpcApiUtil.java | 14 +++--- .../services/jsonrpc/TronJsonRpcImpl.java | 2 +- .../jsonrpc/filters/LogFilterWrapper.java | 2 +- .../tron/core/db/TransactionRetStoreTest.java | 10 ++-- .../jsonrpc/JsonRpcPrunedHistoryTest.java | 46 +++++++++---------- 9 files changed, 49 insertions(+), 46 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 35fcd73bb6d..e95bb3119e9 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -246,9 +246,10 @@ public class ChainBaseManager { @Setter private long lowestBlockNum = -1; // except num = 0. - // lowest block with receipts; above lowestBlockNum on a LiteNode + // first block of transactionRetStore; head + 1 when the store is empty, + // Long.MAX_VALUE when receipts are not persisted @Getter - private long lowestReceiptBlockNum = -1; + private long lowestBlockNumOfReceiptStore = -1; @Getter @Setter @@ -404,16 +405,16 @@ private void init() { } /** - * Probes the receipt floor from the store itself, not from snapshot metadata; an empty + * Probes the lowest receipt block from the store itself, not from snapshot metadata; an empty * store means receipts begin with the next executed block. With receipt persistence off - * the store never grows, so no floor exists. Must run after checkpoint recovery (so the + * the store never grows, so no lower bound exists. Must run after checkpoint recovery (so the * last session's tail is visible) and before any session is built ({@code getNext} does * not merge in-flight layers). */ - public void probeLowestReceiptBlockNum() { + public void probeLowestBlockNumOfReceiptStore() { boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() .getStorage().getTransactionHistorySwitch()); - this.lowestReceiptBlockNum = persistReceipts + this.lowestBlockNumOfReceiptStore = persistReceipts ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) : Long.MAX_VALUE; } diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index ec9ceb5fc7a..a5e27fa36b0 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -41,7 +41,7 @@ public void put(byte[] key, TransactionRetCapsule item) { /** * Lowest block number that has receipts, or empty when the store has none. On a LiteNode - * this is generally above the block floor: a snapshot ships block bodies but no receipts. + * this is generally above the lowest block: a snapshot ships block bodies but no receipts. * *

Startup probe only — must run before any session is built. With in-flight snapshot * layers, {@code getNext} does not merge deletions correctly. diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 51663abd52b..1b8d1a73ec3 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -746,8 +746,8 @@ public long getLowestBlockNum() { return chainBaseManager.getLowestBlockNum(); } - public long getLowestReceiptBlockNum() { - return chainBaseManager.getLowestReceiptBlockNum(); + public long getLowestBlockNumOfReceiptStore() { + return chainBaseManager.getLowestBlockNumOfReceiptStore(); } public BlockCapsule getBlockCapsuleByNum(long blockNum) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 8c626f9b0ab..16faf0c359f 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -496,7 +496,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); - chainBaseManager.probeLowestReceiptBlockNum(); + chainBaseManager.probeLowestBlockNumOfReceiptStore(); transactionCache.initCache(); rewardViCalService.init(); this.setProposalController(ProposalController.createInstance(this)); 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 17dc42f30e6..b6001b6bf89 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 @@ -705,15 +705,16 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) /** * The lowest block for which everything the node persists is available; what "earliest" * resolves to. On a LiteNode this is the - * receipt floor when receipts are persisted, otherwise the body floor (receipt endpoints + * lowest receipt block when receipts are persisted, otherwise the lowest block (receipt endpoints * answer 4444 on such a node regardless of this value). On a FullNode it is genesis. */ public static long earliestAvailableBlock(Wallet wallet) { if (!wallet.isLiteNode()) { return 0; } - long receiptFloor = wallet.getLowestReceiptBlockNum(); - return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor; + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + return lowestReceiptBlockNum == Long.MAX_VALUE + ? wallet.getLowestBlockNum() : lowestReceiptBlockNum; } /** @@ -729,14 +730,15 @@ public static void checkPrunedHistory(long blockNum, Wallet wallet) /** * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read - * receipts or logs; their floor is the first block with receipts. Same raw-primitive + * receipts or logs; their lower bound is the lowest receipt block. Same raw-primitive * contract. Receipt persistence is a per-node switch independent of node type, so a node * that never persists receipts is rejected before the LiteNode gate. */ public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) throws JsonRpcPrunedHistoryException { - long receiptFloor = wallet.getLowestReceiptBlockNum(); - if (receiptFloor == Long.MAX_VALUE || wallet.isLiteNode() && blockNum < receiptFloor) { + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + if (lowestReceiptBlockNum == Long.MAX_VALUE + || wallet.isLiteNode() && blockNum < lowestReceiptBlockNum) { throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); } } 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 c99c8cbec37..88a1cb7040a 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 @@ -958,7 +958,7 @@ public List getBlockReceipts(String blockNumOrHashOrTag) BlockCapsule blockCapsule = new BlockCapsule(block); long blockNum = blockCapsule.getNum(); - // below the receipt floor the body exists but the receipts do not — 4444, not -32000 + // below the lowest receipt block the body exists but the receipts do not — 4444, not -32000 JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet); TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum); 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 4d3871d5479..083b86b1898 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 @@ -105,7 +105,7 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet this.fromBlock = fromBlockSrc; this.toBlock = toBlockSrc; - // Reject a range starting below the receipt floor with 4444. Exception: a genesis-only + // Reject a range starting below the lowest receipt block with 4444. Exception: a genesis-only // query (from = to = 0, or the genesis blockHash) — block 0 is retained. if (wallet != null && !(fromBlockSrc == 0 && toBlockSrc == 0)) { JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet); diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index 0edf429e0b5..bc089a86f48 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java @@ -85,18 +85,18 @@ public void getLowestBlockNumOnEmptyStore() { } @Test - public void probeLowestReceiptBlockNumReadsStore() { + public void probeLowestBlockNumOfReceiptStoreReadsStore() { // the probe runs after checkpoint recovery, so it must reflect whatever the store holds // at call time: the first key while present, the next block once the store is empty - chainBaseManager.probeLowestReceiptBlockNum(); - Assert.assertEquals(1L, chainBaseManager.getLowestReceiptBlockNum()); + chainBaseManager.probeLowestBlockNumOfReceiptStore(); + Assert.assertEquals(1L, chainBaseManager.getLowestBlockNumOfReceiptStore()); // head must be non-zero, otherwise head + 1 collides with the first key asserted above transactionRetStore.delete(blockNum); chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(5); try { - chainBaseManager.probeLowestReceiptBlockNum(); - Assert.assertEquals(6L, chainBaseManager.getLowestReceiptBlockNum()); + chainBaseManager.probeLowestBlockNumOfReceiptStore(); + Assert.assertEquals(6L, chainBaseManager.getLowestBlockNumOfReceiptStore()); } finally { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(0); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java index d7c70f55825..887f82e8e8a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -26,14 +26,14 @@ public class JsonRpcPrunedHistoryTest { private static final long LOWEST_BLOCK_NUM = 100L; - private static final long RECEIPT_FLOOR_BLOCK_NUM = 150L; + private static final long LOWEST_RECEIPT_BLOCK_NUM = 150L; private static final long HEAD_BLOCK_NUM = 200L; private static final String PRUNED_MESSAGE = "Pruned history unavailable"; private static final String BELOW_CUTOFF_HEX = "0x10"; private static final String AT_CUTOFF_HEX = "0x64"; private static final long IN_RECEIPT_GAP_NUM = 112L; private static final String IN_RECEIPT_GAP_HEX = "0x70"; - private static final String RECEIPT_FLOOR_HEX = "0x96"; + private static final String LOWEST_RECEIPT_BLOCK_HEX = "0x96"; private TronJsonRpcImpl rpc; @@ -58,8 +58,8 @@ private static Wallet newMockWallet(boolean liteNode) { Wallet wallet = mock(Wallet.class); when(wallet.isLiteNode()).thenReturn(liteNode); when(wallet.getLowestBlockNum()).thenReturn(liteNode ? LOWEST_BLOCK_NUM : 0L); - when(wallet.getLowestReceiptBlockNum()) - .thenReturn(liteNode ? RECEIPT_FLOOR_BLOCK_NUM : 0L); + when(wallet.getLowestBlockNumOfReceiptStore()) + .thenReturn(liteNode ? LOWEST_RECEIPT_BLOCK_NUM : 0L); when(wallet.getCursor()).thenReturn(Chainbase.Cursor.HEAD); when(wallet.getNowBlock()).thenReturn(newBlock(HEAD_BLOCK_NUM, 0)); // a LiteNode snapshot copies genesis explicitly, so block 0 stays retrievable below the cutoff @@ -78,13 +78,13 @@ private static Wallet newHistoryOffMockWallet() { private static Wallet newHistoryOffMockWallet(boolean liteNode) { Wallet wallet = newMockWallet(liteNode); - when(wallet.getLowestReceiptBlockNum()).thenReturn(Long.MAX_VALUE); + when(wallet.getLowestBlockNumOfReceiptStore()).thenReturn(Long.MAX_VALUE); return wallet; } @Test public void testParseBlockTagEarliestOnLiteNode() throws Exception { - Assert.assertEquals(RECEIPT_FLOOR_BLOCK_NUM, + Assert.assertEquals(LOWEST_RECEIPT_BLOCK_NUM, JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(true))); } @@ -276,7 +276,7 @@ public void testGetBlockReceiptsInReceiptGapReturns4444() { @Test public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { - // the floor check ignores the transaction count, so an empty block is not special-cased + // the check ignores the transaction count, so an empty block is not special-cased Wallet wallet = newMockWallet(true); when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0)); @@ -289,15 +289,15 @@ public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { } @Test - public void testGetBlockReceiptsAtReceiptFloorPasses() throws Exception { + public void testGetBlockReceiptsAtLowestReceiptBlockPasses() throws Exception { Wallet wallet = newMockWallet(true); - when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) - .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); - when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM)) + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(LOWEST_RECEIPT_BLOCK_NUM)) .thenReturn(TransactionInfoList.getDefaultInstance()); rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); - Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertNotNull(rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); } @Test @@ -311,7 +311,7 @@ public void testGetBlockTransactionCountInReceiptGapPasses() throws Exception { } @Test - public void testParseBlockTagEarliestWithHistoryOffFallsBackToBodyFloor() throws Exception { + public void testParseBlockTagEarliestWithHistoryOffFallsBackToLowestBlock() throws Exception { Assert.assertEquals(LOWEST_BLOCK_NUM, JsonRpcApiUtil.parseBlockTag("earliest", newHistoryOffMockWallet())); } @@ -326,7 +326,7 @@ public void testGetBlockByNumberEarliestWithHistoryOffPasses() throws Exception @Test public void testGetLogsWithHistoryOffReturns4444() { rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); - FilterRequest fr = new FilterRequest(RECEIPT_FLOOR_HEX, "latest", null, null, null); + FilterRequest fr = new FilterRequest(LOWEST_RECEIPT_BLOCK_HEX, "latest", null, null, null); JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, () -> rpc.getLogs(fr)); @@ -337,12 +337,12 @@ public void testGetLogsWithHistoryOffReturns4444() { @Test public void testGetBlockReceiptsWithHistoryOffReturns4444() { Wallet wallet = newHistoryOffMockWallet(); - when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) - .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 1)); rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, - () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); Assert.assertNull(e.getData()); } @@ -358,12 +358,12 @@ public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() { @Test public void testGetBlockReceiptsEmptyBlockWithHistoryOffReturns4444() { Wallet wallet = newHistoryOffMockWallet(); - when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) - .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 0)); rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, - () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); Assert.assertNull(e.getData()); } @@ -383,12 +383,12 @@ public void testGetLogsOnFullNodeWithHistoryOffReturns4444() { @Test public void testGetBlockReceiptsOnFullNodeWithHistoryOffReturns4444() { Wallet wallet = newHistoryOffMockWallet(false); - when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) - .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 1)); rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, - () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); Assert.assertNull(e.getData()); } From 62df032602962f6f056a11c31cb50583dd258de1 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 28 Sep 2026 18:38:23 +0800 Subject: [PATCH 4/7] refactor(chainbase): rename to initLowestBlockNumOfReceiptStore and move init methods to the top - rename probeLowestBlockNumOfReceiptStore to initLowestBlockNumOfReceiptStore - move init and initLowestBlockNumOfReceiptStore to the top of ChainBaseManager methods --- .../java/org/tron/core/ChainBaseManager.java | 46 +++++++++---------- .../tron/core/store/TransactionRetStore.java | 2 +- .../main/java/org/tron/core/db/Manager.java | 2 +- .../tron/core/db/TransactionRetStoreTest.java | 8 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index e95bb3119e9..ab7e4a99083 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -255,6 +255,29 @@ public class ChainBaseManager { @Setter private long latestSaveBlockTime; + @PostConstruct + private void init() { + this.lowestBlockNum = this.blockIndexStore.getLimitNumber(1, 1).stream() + .map(BlockId::getNum).findFirst().orElse(0L); + this.nodeType = getLowestBlockNum() > 1 ? NodeType.LITE : NodeType.FULL; + this.latestSaveBlockTime = System.currentTimeMillis(); + } + + /** + * Reads the lowest receipt block from the store itself, not from snapshot metadata; an empty + * store means receipts begin with the next executed block. With receipt persistence off + * the store never grows, so no lower bound exists. Must run after checkpoint recovery (so the + * last session's tail is visible) and before any session is built ({@code getNext} does + * not merge in-flight layers). + */ + public void initLowestBlockNumOfReceiptStore() { + boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() + .getStorage().getTransactionHistorySwitch()); + this.lowestBlockNumOfReceiptStore = persistReceipts + ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) + : Long.MAX_VALUE; + } + // for test only public List getWitnesses() { return witnessScheduleStore.getActiveWitnesses(); @@ -396,29 +419,6 @@ public long getNextBlockSlotTime() { + slotCount * BLOCK_PRODUCED_INTERVAL; } - @PostConstruct - private void init() { - this.lowestBlockNum = this.blockIndexStore.getLimitNumber(1, 1).stream() - .map(BlockId::getNum).findFirst().orElse(0L); - this.nodeType = getLowestBlockNum() > 1 ? NodeType.LITE : NodeType.FULL; - this.latestSaveBlockTime = System.currentTimeMillis(); - } - - /** - * Probes the lowest receipt block from the store itself, not from snapshot metadata; an empty - * store means receipts begin with the next executed block. With receipt persistence off - * the store never grows, so no lower bound exists. Must run after checkpoint recovery (so the - * last session's tail is visible) and before any session is built ({@code getNext} does - * not merge in-flight layers). - */ - public void probeLowestBlockNumOfReceiptStore() { - boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() - .getStorage().getTransactionHistorySwitch()); - this.lowestBlockNumOfReceiptStore = persistReceipts - ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) - : Long.MAX_VALUE; - } - public void shutdown() { dbStatService.shutdown(); } diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index a5e27fa36b0..d6ff69367b9 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -43,7 +43,7 @@ public void put(byte[] key, TransactionRetCapsule item) { * Lowest block number that has receipts, or empty when the store has none. On a LiteNode * this is generally above the lowest block: a snapshot ships block bodies but no receipts. * - *

Startup probe only — must run before any session is built. With in-flight snapshot + *

Startup use only — must run before any session is built. With in-flight snapshot * layers, {@code getNext} does not merge deletions correctly. */ public OptionalLong getLowestBlockNum() { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 16faf0c359f..a76e6a198c3 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -496,7 +496,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); - chainBaseManager.probeLowestBlockNumOfReceiptStore(); + chainBaseManager.initLowestBlockNumOfReceiptStore(); transactionCache.initCache(); rewardViCalService.init(); this.setProposalController(ProposalController.createInstance(this)); diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index bc089a86f48..b08543253f8 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java @@ -85,17 +85,17 @@ public void getLowestBlockNumOnEmptyStore() { } @Test - public void probeLowestBlockNumOfReceiptStoreReadsStore() { - // the probe runs after checkpoint recovery, so it must reflect whatever the store holds + public void initLowestBlockNumOfReceiptStoreReadsStore() { + // the init runs after checkpoint recovery, so it must reflect whatever the store holds // at call time: the first key while present, the next block once the store is empty - chainBaseManager.probeLowestBlockNumOfReceiptStore(); + chainBaseManager.initLowestBlockNumOfReceiptStore(); Assert.assertEquals(1L, chainBaseManager.getLowestBlockNumOfReceiptStore()); // head must be non-zero, otherwise head + 1 collides with the first key asserted above transactionRetStore.delete(blockNum); chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(5); try { - chainBaseManager.probeLowestBlockNumOfReceiptStore(); + chainBaseManager.initLowestBlockNumOfReceiptStore(); Assert.assertEquals(6L, chainBaseManager.getLowestBlockNumOfReceiptStore()); } finally { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(0); From b48975b6ffa6bf49d6fb18f5c124059f4096f643 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 28 Sep 2026 18:52:20 +0800 Subject: [PATCH 5/7] refactor(jsonrpc): simplify pruned-history conditions --- .../java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java | 2 +- .../tron/core/services/jsonrpc/filters/LogFilterWrapper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index b6001b6bf89..6d87a3ff29b 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 @@ -738,7 +738,7 @@ public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) throws JsonRpcPrunedHistoryException { long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); if (lowestReceiptBlockNum == Long.MAX_VALUE - || wallet.isLiteNode() && blockNum < lowestReceiptBlockNum) { + || (wallet.isLiteNode() && blockNum < lowestReceiptBlockNum)) { throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); } } 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 083b86b1898..c45ed8d0915 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 @@ -107,7 +107,7 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet // Reject a range starting below the lowest receipt block with 4444. Exception: a genesis-only // query (from = to = 0, or the genesis blockHash) — block 0 is retained. - if (wallet != null && !(fromBlockSrc == 0 && toBlockSrc == 0)) { + if (wallet != null && (fromBlockSrc != 0 || toBlockSrc != 0)) { JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet); } From a795458c02b80a1605e9530cacb6f78ba15ac1f9 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 8 Oct 2026 15:45:52 +0800 Subject: [PATCH 6/7] refactor(jsonrpc): rename pruned-history helpers --- .../org/tron/core/services/jsonrpc/JsonRpcApiUtil.java | 8 ++++---- .../org/tron/core/services/jsonrpc/TronJsonRpcImpl.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 6d87a3ff29b..c54d2492cd5 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 @@ -638,7 +638,7 @@ public static long parseBlockTag(String tag, Wallet wallet) return wallet.getHeadBlockNum(); } if (EARLIEST_STR.equalsIgnoreCase(tag)) { - return earliestAvailableBlock(wallet); + return resolveEarliestBlockNum(wallet); } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -708,7 +708,7 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) * lowest receipt block when receipts are persisted, otherwise the lowest block (receipt endpoints * answer 4444 on such a node regardless of this value). On a FullNode it is genesis. */ - public static long earliestAvailableBlock(Wallet wallet) { + public static long resolveEarliestBlockNum(Wallet wallet) { if (!wallet.isLiteNode()) { return 0; } @@ -721,7 +721,7 @@ public static long earliestAvailableBlock(Wallet wallet) { * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444. * Raw primitive — no genesis exemption; callers own that semantics. */ - public static void checkPrunedHistory(long blockNum, Wallet wallet) + public static void checkPrunedBlockHistory(long blockNum, Wallet wallet) throws JsonRpcPrunedHistoryException { if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); @@ -729,7 +729,7 @@ public static void checkPrunedHistory(long blockNum, Wallet wallet) } /** - * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read + * Receipt form of {@link #checkPrunedBlockHistory(long, Wallet)} for endpoints that read * receipts or logs; their lower bound is the lowest receipt block. Same raw-primitive * contract. Receipt persistence is a per-node switch independent of node type, so a node * that never persists receipts is rejected before the LiteNode gate. 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 88a1cb7040a..5154f587b95 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 @@ -414,7 +414,7 @@ private Block getBlockByNumOrTag(String blockNumOrTag) // history it cannot serve. Genesis is exempt: a snapshot copies block 0 explicitly, and // lowestBlockNum is computed from block 1 upwards, so block 0 is always retained. if (blockNum > 0) { - JsonRpcApiUtil.checkPrunedHistory(blockNum, wallet); + JsonRpcApiUtil.checkPrunedBlockHistory(blockNum, wallet); } return wallet.getBlockByNum(blockNum); } From 294c0d4a204b7028f273e3499d7373681b348c6b Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 8 Oct 2026 15:50:14 +0800 Subject: [PATCH 7/7] fix(jsonrpc): validate transaction index before block lookup --- .../services/jsonrpc/TronJsonRpcImpl.java | 11 ++++---- .../jsonrpc/JsonRpcPrunedHistoryTest.java | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) 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 5154f587b95..757f05fb740 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 @@ -823,10 +823,7 @@ private TransactionResult formatTransactionResult(TransactionInfo transactioninf energyUsageTotal, wallet.getEnergyFee(blockCapsule.getTimeStamp()), wallet); } - private TransactionResult getTransactionByBlockAndIndex(Block block, String index) - throws JsonRpcInvalidParamsException { - int txIndex = parseTxIndex(index); - + private TransactionResult getTransactionByBlockAndIndex(Block block, int txIndex) { if (txIndex < 0 || txIndex >= block.getTransactionsCount()) { return null; } @@ -842,24 +839,26 @@ private TransactionResult getTransactionByBlockAndIndex(Block block, String inde @Override public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String index) throws JsonRpcInvalidParamsException { + int txIndex = parseTxIndex(index); final Block block = getBlockByJsonHash(blockHash); if (block == null) { return null; } - return getTransactionByBlockAndIndex(block, index); + return getTransactionByBlockAndIndex(block, txIndex); } @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { + int txIndex = parseTxIndex(index); Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } - return getTransactionByBlockAndIndex(block, index); + return getTransactionByBlockAndIndex(block, txIndex); } /** diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java index 887f82e8e8a..85d78da3738 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -34,6 +34,8 @@ public class JsonRpcPrunedHistoryTest { private static final long IN_RECEIPT_GAP_NUM = 112L; private static final String IN_RECEIPT_GAP_HEX = "0x70"; private static final String LOWEST_RECEIPT_BLOCK_HEX = "0x96"; + private static final String INVALID_INDEX = "1a"; + private static final String UNKNOWN_HASH = "0x" + Strings.repeat("ab", 32); private TronJsonRpcImpl rpc; @@ -168,6 +170,30 @@ public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { Assert.assertNull(e.getData()); } + @Test + public void testGetTransactionByBlockNumberAndIndexInvalidIndexBelowCutoff() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, INVALID_INDEX)); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexInvalidIndexMissingBlock() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(AT_CUTOFF_HEX, INVALID_INDEX)); + } + + @Test + public void testGetTransactionByBlockHashAndIndexInvalidIndexMissingBlock() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockHashAndIndex(UNKNOWN_HASH, INVALID_INDEX)); + } + @Test public void testGetBlockReceiptsBelowCutoffReturns4444() { TronJsonRpcImpl liteRpc = newRpc(true);