diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 21f0bac8d77..ab7e4a99083 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,10 +246,38 @@ public class ChainBaseManager { @Setter private long lowestBlockNum = -1; // except num = 0. + // first block of transactionRetStore; head + 1 when the store is empty, + // Long.MAX_VALUE when receipts are not persisted + @Getter + private long lowestBlockNumOfReceiptStore = -1; + @Getter @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(); @@ -389,14 +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(); - } - 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..d6ff69367b9 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,9 @@ package org.tron.core.store; +import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; 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 +39,18 @@ 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 use only — must run before any session is built. With in-flight snapshot + * layers, {@code getNext} does not merge deletions correctly. + */ + public OptionalLong getLowestBlockNum() { + return revokingDB.getNext(ByteArray.fromLong(0), 1).keySet().stream() + .mapToLong(Longs::fromByteArray).findFirst(); + } + 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..259081b317b --- /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 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 { + + 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..1b8d1a73ec3 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -236,6 +236,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 +738,18 @@ public long getHeadBlockNum() { return chainBaseManager.getHeadBlockNum(); } + public boolean isLiteNode() { + return chainBaseManager.isLiteNode(); + } + + public long getLowestBlockNum() { + return chainBaseManager.getLowestBlockNum(); + } + + public long getLowestBlockNumOfReceiptStore() { + return chainBaseManager.getLowestBlockNumOfReceiptStore(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -3192,6 +3205,7 @@ public Transaction callConstantContract(TransactionCapsule trxCap, } 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..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,6 +496,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); + chainBaseManager.initLowestBlockNumOfReceiptStore(); 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..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 @@ -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 resolveEarliestBlockNum(wallet); } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -700,6 +702,47 @@ 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. On a LiteNode this is the + * 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 resolveEarliestBlockNum(Wallet wallet) { + if (!wallet.isLiteNode()) { + return 0; + } + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + return lowestReceiptBlockNum == Long.MAX_VALUE + ? wallet.getLowestBlockNum() : lowestReceiptBlockNum; + } + + /** + * 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 checkPrunedBlockHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + } + + /** + * 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. + */ + public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + if (lowestReceiptBlockNum == Long.MAX_VALUE + || (wallet.isLiteNode() && blockNum < lowestReceiptBlockNum)) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + } + /** * 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..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 @@ -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.checkPrunedBlockHistory(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 @@ -814,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; } @@ -833,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 { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { + int txIndex = parseTxIndex(index); Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } - return getTransactionByBlockAndIndex(block, index); + return getTransactionByBlockAndIndex(block, txIndex); } /** @@ -925,11 +933,13 @@ 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 public List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException { + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException { Block block = null; @@ -940,13 +950,15 @@ 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(); + // 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); // energy price at the block timestamp @@ -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..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 @@ -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 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); + } + // 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/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/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index 3a13c7d5606..b08543253f8 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 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.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.initLowestBlockNumOfReceiptStore(); + Assert.assertEquals(6L, chainBaseManager.getLowestBlockNumOfReceiptStore()); + } 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..85d78da3738 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -0,0 +1,429 @@ +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 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 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; + + @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.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 + 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.getLowestBlockNumOfReceiptStore()).thenReturn(Long.MAX_VALUE); + return wallet; + } + + @Test + public void testParseBlockTagEarliestOnLiteNode() throws Exception { + Assert.assertEquals(LOWEST_RECEIPT_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()); + Assert.assertNull(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.assertNull(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.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); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsFutureBlockWithHistoryOffReturnsNull() throws Exception { + // a block that does not exist yet is not pruned history + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + + Assert.assertNull(rpc.getBlockReceipts("0x7fffffff")); + } + + @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.assertNull(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.assertNull(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.assertNull(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.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { + // 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)); + 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.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsAtLowestReceiptBlockPasses() throws Exception { + Wallet wallet = newMockWallet(true); + 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(LOWEST_RECEIPT_BLOCK_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 testParseBlockTagEarliestWithHistoryOffFallsBackToLowestBlock() 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(LOWEST_RECEIPT_BLOCK_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(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(LOWEST_RECEIPT_BLOCK_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(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(LOWEST_RECEIPT_BLOCK_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(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(LOWEST_RECEIPT_BLOCK_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)); + } + } + } + } + +}