diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 21f0bac8d77..2fd05c8b73e 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -9,8 +9,10 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.metric.DbStatService; import org.tron.common.utils.ForkController; import org.tron.common.utils.Sha256Hash; @@ -244,6 +246,11 @@ public class ChainBaseManager { @Setter private long lowestBlockNum = -1; // except num = 0. + // lowest block with receipts; above lowestBlockNum on a LiteNode + @Getter + @Setter + private long lowestReceiptBlockNum = -1; + @Getter @Setter private long latestSaveBlockTime; @@ -397,6 +404,21 @@ private void init() { this.latestSaveBlockTime = System.currentTimeMillis(); } + /** + * Probes the receipt floor from the store itself, not from snapshot metadata; an empty + * store means receipts begin with the next executed block. With receipt persistence off + * the store never grows, so no floor exists. Must run after checkpoint recovery (so the + * last session's tail is visible) and before any session is built ({@code getNext} does + * not merge in-flight layers). + */ + public void probeLowestReceiptBlockNum() { + boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() + .getStorage().getTransactionHistorySwitch()); + this.lowestReceiptBlockNum = persistReceipts + ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) + : Long.MAX_VALUE; + } + public void shutdown() { dbStatService.shutdown(); } diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java index 8ff3064b73c..2f1b1ee80b6 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java @@ -147,6 +147,10 @@ public void setErrorCode(Result.code code) { this.transactionResult = this.transactionResult.toBuilder().setRet(code).build(); } + public void setResultCode(contractResult code) { + this.transactionResult = this.transactionResult.toBuilder().setContractRet(code).build(); + } + public long getShieldedTransactionFee() { return transactionResult.getShieldedTransactionFee(); } @@ -184,4 +188,4 @@ public byte[] getData() { public Result getInstance() { return this.transactionResult; } -} \ No newline at end of file +} diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index a22e69e8692..6b08c9dd69b 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -1,7 +1,10 @@ package org.tron.core.store; +import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; +import java.util.Map; import java.util.Objects; +import java.util.OptionalLong; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +40,23 @@ public void put(byte[] key, TransactionRetCapsule item) { } } + /** + * Lowest block number that has receipts, or empty when the store has none. On a LiteNode + * this is generally above the block floor: a snapshot ships block bodies but no receipts. + * + *

Startup probe only — must run before any session is built. With in-flight snapshot + * layers, {@code getNext} does not merge deletions correctly. + */ + public OptionalLong getLowestBlockNum() { + Map entries = revokingDB.getNext(ByteArray.fromLong(0), 1); + for (byte[] key : entries.keySet()) { + if (key.length == Long.BYTES) { + return OptionalLong.of(Longs.fromByteArray(key)); + } + } + return OptionalLong.empty(); + } + public TransactionInfoCapsule getTransactionInfo(byte[] key) throws BadItemException { long blockNumber = transactionStore.getBlockNumber(key); if (blockNumber == -1) { diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java new file mode 100644 index 00000000000..5a090e76705 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java @@ -0,0 +1,12 @@ +package org.tron.core.exception.jsonrpc; + +public class JsonRpcExecutionRevertedException extends JsonRpcException { + + public JsonRpcExecutionRevertedException(String message) { + super(message); + } + + public JsonRpcExecutionRevertedException(String message, Object data) { + super(message, data); + } +} diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java new file mode 100644 index 00000000000..d5cf4f0c887 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java @@ -0,0 +1,17 @@ +package org.tron.core.exception.jsonrpc; + +/** + * Thrown when a request targets historical state that a LiteNode has pruned. + * Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized + * by the Ethereum Execution API (EIP-4444). + */ +public class JsonRpcPrunedHistoryException extends JsonRpcException { + + public JsonRpcPrunedHistoryException(String message) { + super(message); + } + + public JsonRpcPrunedHistoryException(String message, Object data) { + super(message, data); + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index ac54cb2b7ff..f1d24740269 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -201,6 +201,7 @@ import org.tron.core.store.StoreFactory; import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; +import org.tron.core.utils.ResultCodeUtil; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; @@ -236,6 +237,7 @@ import org.tron.protos.Protocol.Transaction.Contract; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; import org.tron.protos.contract.BalanceContract; @@ -737,6 +739,18 @@ public long getHeadBlockNum() { return chainBaseManager.getHeadBlockNum(); } + public boolean isLiteNode() { + return chainBaseManager.isLiteNode(); + } + + public long getLowestBlockNum() { + return chainBaseManager.getLowestBlockNum(); + } + + public long getLowestReceiptBlockNum() { + return chainBaseManager.getLowestReceiptBlockNum(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -3186,12 +3200,15 @@ public Transaction callConstantContract(TransactionCapsule trxCap, ret.setStatus(0, code.SUCESS); if (StringUtils.isNoneEmpty(result.getRuntimeError())) { ret.setStatus(0, code.FAILED); + // same failure classification as executed transactions + ret.setResultCode(ResultCodeUtil.resolve(result.getException())); retBuilder .setMessage(ByteString.copyFromUtf8(result.getRuntimeError())) .build(); } if (result.isRevert()) { ret.setStatus(0, code.FAILED); + ret.setResultCode(contractResult.REVERT); retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed")) .build(); } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 9d7a7c979b9..8c626f9b0ab 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -496,6 +496,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); + chainBaseManager.probeLowestReceiptBlockNum(); transactionCache.initCache(); rewardViCalService.init(); this.setProposalController(ProposalController.createInstance(this)); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index f4bba9fbf37..18eff9b2de7 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,14 @@ public static long parseBlockTag(String tag, Wallet wallet) return wallet.getHeadBlockNum(); } if (EARLIEST_STR.equalsIgnoreCase(tag)) { - return 0; + if (!wallet.isLiteNode()) { + return 0; + } + // "earliest" is the lowest block for which everything the node persists is available: + // the receipt floor when receipts are persisted, otherwise the body floor (receipt + // endpoints answer 4444 on such a node regardless of this value) + long receiptFloor = wallet.getLowestReceiptBlockNum(); + return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor; } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -700,6 +709,43 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) return parseBlockNumber(blockNumOrTag); } + /** + * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444. + * Raw primitive — no genesis exemption; callers own that semantics. + */ + public static void checkPrunedHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { + throw prunedHistory(wallet.getLowestBlockNum()); + } + } + + /** + * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read + * receipts or logs; their floor is the first block with receipts. Same raw-primitive + * contract. Receipt persistence is a per-node switch independent of node type, so a node + * that never persists receipts is rejected before the LiteNode gate. + */ + public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + long receiptFloor = wallet.getLowestReceiptBlockNum(); + if (receiptFloor == Long.MAX_VALUE) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + if (wallet.isLiteNode() && blockNum < receiptFloor) { + throw prunedHistory(receiptFloor); + } + } + + /** + * The Execution API fixes the message verbatim; the earliest available block travels in + * {@code data} so a client can pick a fallback node from it. + */ + private static JsonRpcPrunedHistoryException prunedHistory(long earliestAvailable) { + return new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR, + "0x" + Long.toHexString(earliestAvailable)); + } + /** * Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the * longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix. diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 50da763b8b9..6e03361e3ae 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java @@ -19,10 +19,12 @@ import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException; import org.tron.core.services.jsonrpc.types.BlockResult; import org.tron.core.services.jsonrpc.types.BuildArguments; @@ -55,8 +57,10 @@ public interface TronJsonRpc { @JsonRpcMethod("eth_getBlockTransactionCountByNumber") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) - String ethGetBlockTransactionCountByNumber(String bnOrId) throws JsonRpcInvalidParamsException; + String ethGetBlockTransactionCountByNumber(String bnOrId) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getBlockByHash") @JsonRpcErrors({ @@ -68,9 +72,10 @@ BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionObjects) @JsonRpcMethod("eth_getBlockByNumber") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) BlockResult ethGetBlockByNumber(String bnOrId, Boolean fullTransactionObjects) - throws JsonRpcInvalidParamsException; + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("net_version") String getNetVersion() throws JsonRpcInternalException; @@ -120,10 +125,12 @@ String getABIOfSmartContract(String contractAddress, String bnOrId) @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException; + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException; @JsonRpcMethod("eth_getTransactionByHash") @JsonRpcErrors({ @@ -141,9 +148,10 @@ TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String ind @JsonRpcMethod("eth_getTransactionByBlockNumberAndIndex") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) - throws JsonRpcInvalidParamsException; + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getTransactionReceipt") @JsonRpcErrors({ @@ -154,20 +162,23 @@ TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, Stri @JsonRpcMethod("eth_getBlockReceipts") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}") }) List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException; + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_call") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String getCall(CallArguments transactionCall, Object blockNumOrTag) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, - JsonRpcInternalException; + JsonRpcInternalException, JsonRpcExecutionRevertedException; @JsonRpcMethod("net_peerCount") String getPeerCount(); @@ -292,9 +303,10 @@ CompilationResult ethSubmitHashrate(String hashrate, String id) @JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), @JsonRpcError(exception = JsonRpcExceedLimitException.class, code = -32005, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), }) String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, - JsonRpcMethodNotFoundException, JsonRpcExceedLimitException; + JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_newBlockFilter") @JsonRpcErrors({ @@ -327,6 +339,7 @@ Object[] getFilterChanges(String filterId) @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), @JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"), @JsonRpcError(exception = JsonRpcTooManyResultException.class, code = -32005, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), @JsonRpcError(exception = BadItemException.class, code = -32000, data = "{}"), @JsonRpcError(exception = ExecutionException.class, code = -32000, data = "{}"), @JsonRpcError(exception = InterruptedException.class, code = -32000, data = "{}"), @@ -334,7 +347,8 @@ Object[] getFilterChanges(String filterId) }) LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException, ExecutionException, InterruptedException, BadItemException, ItemNotFoundException, - JsonRpcMethodNotFoundException, JsonRpcTooManyResultException; + JsonRpcMethodNotFoundException, JsonRpcTooManyResultException, + JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_getFilterLogs") @JsonRpcErrors({ diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index 6be47886117..e5e57b02838 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -71,10 +71,12 @@ import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.VMIllegalException; import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.http.JsonFormat; @@ -101,6 +103,7 @@ import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.TransferAssetContract; import org.tron.protos.contract.BalanceContract.TransferContract; @@ -349,7 +352,7 @@ public String ethGetBlockTransactionCountByHash(String blockHash) @Override public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; @@ -368,7 +371,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { final Block b = getBlockByNumOrTag(blockNumOrTag); return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } @@ -394,16 +397,26 @@ private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsEx return wallet.getBlockById(ByteString.copyFrom(bHash)); } - private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + private Block getBlockByNumOrTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { + long blockNum; if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { // Return the head block directly from blockStore, bypassing blockIndexStore // which may not yet be written when latestBlockHeaderNumber is already updated. return wallet.getNowBlock(); } - return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); + blockNum = JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet); + } else { + blockNum = parseBlockNumber(blockNumOrTag); } - return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag)); + // Reject a pruned height before touching any store, so a LiteNode pays no lookup for + // history it cannot serve. Genesis is exempt: a snapshot copies block 0 explicitly, and + // lowestBlockNum is computed from block 1 upwards, so block 0 is always retained. + if (blockNum > 0) { + JsonRpcApiUtil.checkPrunedHistory(blockNum, wallet); + } + return wallet.getBlockByNum(blockNum); } private BlockResult getBlockResult(Block block, boolean fullTx) { @@ -543,12 +556,33 @@ static String tryDecodeRevertReason(byte[] resData) { } } + /** + * Rejects a failed constant-call execution: throws code 3 with the revert payload in data + * for a contract revert, -32000 for any other execution failure. + */ + private void requireExecutionSuccess(TransactionExtention.Builder trxExtBuilder, + Return.Builder retBuilder) + throws JsonRpcInternalException, JsonRpcExecutionRevertedException { + Transaction.Result txResult = trxExtBuilder.getTransaction().getRet(0); + if (txResult.getRet().equals(code.SUCESS)) { + return; + } + byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); + if (txResult.getContractRet() == contractResult.REVERT) { + throw new JsonRpcExecutionRevertedException(errMsg, ByteArray.toJsonHex(resData)); + } + throw new JsonRpcInternalException(errMsg, + resData.length > 0 ? ByteArray.toJsonHex(resData) : null); + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 */ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long value, - byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException { + byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); Return.Builder retBuilder = Return.newBuilder(); @@ -577,27 +611,14 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va trxExt = trxExtBuilder.build(); } - String result; - if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.SUCESS)) { - List list = trxExt.getConstantResultList(); - byte[] listBytes = new byte[0]; - for (ByteString bs : list) { - listBytes = ByteUtil.merge(listBytes, bs.toByteArray()); - } - result = ByteArray.toJsonHex(listBytes); - } else { - byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); - - if (resData.length > 0) { - throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); - } else { - throw new JsonRpcInternalException(errMsg); - } + requireExecutionSuccess(trxExtBuilder, retBuilder); + List list = trxExt.getConstantResultList(); + byte[] listBytes = new byte[0]; + for (ByteString bs : list) { + listBytes = ByteUtil.merge(listBytes, bs.toByteArray()); } - - return result; + return ByteArray.toJsonHex(listBytes); } @Override @@ -672,7 +693,8 @@ public String gasPrice() { @Override public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException { + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { byte[] ownerAddress = addressCompatibleToByteArray(args.getFrom()); ContractType contractType = args.getContractType(wallet); @@ -730,25 +752,12 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept throw new JsonRpcInternalException(errString); } - if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); - - if (data.length > 0) { - throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); - } else { - throw new JsonRpcInternalException(errMsg); - } - - } else { - - if (supportEstimateEnergy) { - return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired()); - } else { - return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed()); - } + requireExecutionSuccess(trxExtBuilder, retBuilder); + if (supportEstimateEnergy) { + return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired()); } + return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed()); } @Override @@ -844,7 +853,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; @@ -929,7 +938,8 @@ private TransactionContext findTransactionContext(TransactionInfoList infoList, */ @Override public List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException { + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException { Block block = null; @@ -947,13 +957,18 @@ public List getBlockReceipts(String blockNumOrHashOrTag) BlockCapsule blockCapsule = new BlockCapsule(block); long blockNum = blockCapsule.getNum(); + int transactionSizeInBlock = blockCapsule.getTransactions().size(); + // an empty block trivially has no receipts; for the rest, below the receipt floor the + // body exists but the receipts do not — 4444, not -32000 + if (transactionSizeInBlock > 0) { + JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet); + } TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum); // energy price at the block timestamp long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp()); // Validate transaction list size consistency - int transactionSizeInBlock = blockCapsule.getTransactions().size(); if (transactionSizeInBlock != transactionInfoList.getTransactionInfoCount()) { throw new JsonRpcInternalException( String.format("TransactionList size mismatch: " @@ -1000,7 +1015,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 +1453,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 +1553,8 @@ public Object[] getFilterChanges(String filterId) throws ItemNotFoundException, @Override public LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException, ExecutionException, InterruptedException, BadItemException, ItemNotFoundException, - JsonRpcMethodNotFoundException, JsonRpcTooManyResultException { + JsonRpcMethodNotFoundException, JsonRpcTooManyResultException, + JsonRpcPrunedHistoryException { disableInPBFT("eth_getLogs"); long currentMaxBlockNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java index 57739819d1e..6c3296e5c0c 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java @@ -6,6 +6,7 @@ import lombok.Getter; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; @@ -15,7 +16,7 @@ public class LogFilterAndResult extends FilterResult { private final LogFilterWrapper logFilterWrapper; public LogFilterAndResult(FilterRequest fr, long currentMaxBlockNum, Wallet wallet) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // eth_newFilter, no need to check block range this.logFilterWrapper = new LogFilterWrapper(fr, currentMaxBlockNum, wallet, false); result = new LinkedBlockingQueue<>(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 0fdf174bb50..4d3871d5479 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -9,6 +9,7 @@ import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.JsonRpcApiUtil; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.protos.Protocol.Block; @@ -25,7 +26,8 @@ public class LogFilterWrapper { private final long toBlock; public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet, - boolean checkBlockRange) throws JsonRpcInvalidParamsException { + boolean checkBlockRange) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // 1.convert FilterRequest to LogFilter this.logFilter = new LogFilter(fr); @@ -103,6 +105,12 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet this.fromBlock = fromBlockSrc; this.toBlock = toBlockSrc; + // Reject a range starting below the receipt floor with 4444. Exception: a genesis-only + // query (from = to = 0, or the genesis blockHash) — block 0 is retained. + if (wallet != null && !(fromBlockSrc == 0 && toBlockSrc == 0)) { + JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet); + } + // eth_getLogs enforces the block range at construction time. eth_newFilter creates the // wrapper with checkBlockRange=false (no creation-time gate); eth_getFilterLogs re-runs this // check against the current head before scanning so the cap cannot be bypassed. diff --git a/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java new file mode 100644 index 00000000000..ec2703331af --- /dev/null +++ b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java @@ -0,0 +1,48 @@ +package org.tron.core.utils; + +import org.tron.core.vm.program.Program; +import org.tron.protos.Protocol.Transaction.Result.contractResult; + +/** + * Maps a TVM execution exception to its {@code contractResult} code for constant-call + * responses. Deliberately independent of RuntimeImpl's consensus-path classification. + */ +public class ResultCodeUtil { + + public static contractResult resolve(RuntimeException exception) { + if (exception instanceof Program.IllegalOperationException) { + return contractResult.ILLEGAL_OPERATION; + } + if (exception instanceof Program.OutOfEnergyException) { + return contractResult.OUT_OF_ENERGY; + } + if (exception instanceof Program.BadJumpDestinationException) { + return contractResult.BAD_JUMP_DESTINATION; + } + if (exception instanceof Program.OutOfTimeException) { + return contractResult.OUT_OF_TIME; + } + if (exception instanceof Program.OutOfMemoryException) { + return contractResult.OUT_OF_MEMORY; + } + if (exception instanceof Program.PrecompiledContractException) { + return contractResult.PRECOMPILED_CONTRACT; + } + if (exception instanceof Program.StackTooSmallException) { + return contractResult.STACK_TOO_SMALL; + } + if (exception instanceof Program.StackTooLargeException) { + return contractResult.STACK_TOO_LARGE; + } + if (exception instanceof Program.JVMStackOverFlowException) { + return contractResult.JVM_STACK_OVER_FLOW; + } + if (exception instanceof Program.TransferException) { + return contractResult.TRANSFER_FAILED; + } + if (exception instanceof Program.InvalidCodeException) { + return contractResult.INVALID_CODE; + } + return contractResult.UNKNOWN; + } +} diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index 3a13c7d5606..bf9120c8699 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,44 @@ public void before() { transactionStore.put(transactionId, transactionCapsule); } + @Test + public void getLowestBlockNum() { + Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong()); + } + + @Test + public void getLowestBlockNumPicksMinimumKey() { + transactionRetStore.put(ByteArray.fromLong(7), transactionRetCapsule); + transactionRetStore.put(ByteArray.fromLong(3), transactionRetCapsule); + try { + Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong()); + transactionRetStore.delete(blockNum); + Assert.assertEquals(3L, transactionRetStore.getLowestBlockNum().getAsLong()); + } finally { + transactionRetStore.delete(ByteArray.fromLong(3)); + transactionRetStore.delete(ByteArray.fromLong(7)); + } + } + + @Test + public void getLowestBlockNumOnEmptyStore() { + transactionRetStore.delete(blockNum); + Assert.assertFalse(transactionRetStore.getLowestBlockNum().isPresent()); + } + + @Test + public void probeLowestReceiptBlockNumReadsStore() { + // the probe runs after checkpoint recovery, so it must reflect whatever the store holds + // at call time: the first key while present, the next block once the store is empty + chainBaseManager.probeLowestReceiptBlockNum(); + Assert.assertEquals(1L, chainBaseManager.getLowestReceiptBlockNum()); + + transactionRetStore.delete(blockNum); + chainBaseManager.probeLowestReceiptBlockNum(); + Assert.assertEquals(chainBaseManager.getHeadBlockNum() + 1, + chainBaseManager.getLowestReceiptBlockNum()); + } + @Test public void get() throws BadItemException { TransactionInfoCapsule resultCapsule = transactionRetStore.getTransactionInfo(transactionId); @@ -78,7 +116,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..e49cd4359e3 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,13 @@ 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.protos.Protocol; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; public class JsonRpcCallAndEstimateGasTest { @@ -55,76 +57,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 +157,29 @@ 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 testGetCallReturnsConstantResult() throws Exception { byte[] part1 = ByteArray.fromHexString("deadbeef"); @@ -173,8 +213,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 +223,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 +236,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 +246,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; }); } 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..71fe3788c4d --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -0,0 +1,393 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Strings; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.TransactionInfoList; +import org.tron.core.Wallet; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; + +public class JsonRpcPrunedHistoryTest { + + private static final long LOWEST_BLOCK_NUM = 100L; + private static final long RECEIPT_FLOOR_BLOCK_NUM = 150L; + private static final long HEAD_BLOCK_NUM = 200L; + private static final String PRUNED_MESSAGE = "Pruned history unavailable"; + private static final String BODY_FLOOR_HEX = "0x64"; + private static final String BELOW_CUTOFF_HEX = "0x10"; + private static final String AT_CUTOFF_HEX = "0x64"; + private static final long IN_RECEIPT_GAP_NUM = 112L; + private static final String IN_RECEIPT_GAP_HEX = "0x70"; + private static final String RECEIPT_FLOOR_HEX = "0x96"; + + private TronJsonRpcImpl rpc; + + @After + public void tearDown() throws IOException { + if (rpc != null) { + rpc.close(); + rpc = null; + } + } + + private static Block newBlock(long number, int transactionCount) { + Block.Builder builder = Block.newBuilder().setBlockHeader(BlockHeader.newBuilder() + .setRawData(BlockHeader.raw.newBuilder().setNumber(number))); + for (int i = 0; i < transactionCount; i++) { + builder.addTransactions(Transaction.newBuilder()); + } + return builder.build(); + } + + private static Wallet newMockWallet(boolean liteNode) { + Wallet wallet = mock(Wallet.class); + when(wallet.isLiteNode()).thenReturn(liteNode); + when(wallet.getLowestBlockNum()).thenReturn(liteNode ? LOWEST_BLOCK_NUM : 0L); + when(wallet.getLowestReceiptBlockNum()) + .thenReturn(liteNode ? RECEIPT_FLOOR_BLOCK_NUM : 0L); + when(wallet.getCursor()).thenReturn(Chainbase.Cursor.HEAD); + when(wallet.getNowBlock()).thenReturn(newBlock(HEAD_BLOCK_NUM, 0)); + // a LiteNode snapshot copies genesis explicitly, so block 0 stays retrievable below the cutoff + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 0)); + return wallet; + } + + private TronJsonRpcImpl newRpc(boolean liteNode) { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newMockWallet(liteNode)); + return rpc; + } + + private static Wallet newHistoryOffMockWallet() { + return newHistoryOffMockWallet(true); + } + + private static Wallet newHistoryOffMockWallet(boolean liteNode) { + Wallet wallet = newMockWallet(liteNode); + when(wallet.getLowestReceiptBlockNum()).thenReturn(Long.MAX_VALUE); + return wallet; + } + + @Test + public void testParseBlockTagEarliestOnLiteNode() throws Exception { + Assert.assertEquals(RECEIPT_FLOOR_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(true))); + } + + @Test + public void testParseBlockTagEarliestOnFullNode() throws Exception { + Assert.assertEquals(0L, JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(false))); + } + + @Test + public void testGetBlockByNumberBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockByNumberAtCutoffPasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber(AT_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockByNumberEarliestOnLiteNodePasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetBlockByNumberOnFullNodePasses() throws Exception { + Assert.assertNull(newRpc(false).ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockTransactionCountGenesisOnLiteNodePasses() throws Exception { + // genesis is retained below the cutoff, so a single-block lookup must not return 4444 + Assert.assertEquals("0x0", newRpc(true).ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetBlockTransactionCountGenesisWithTransactionsOnLiteNode() throws Exception { + // Genesis carries initial-allocation transactions in its body but never has transactionInfo + // (initGenesis writes only blockStore/blockIndexStore, no processBlock). The count endpoint + // reads the body, so it must return the real count, not 4444. + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 2)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertEquals("0x2", rpc.ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexGenesisOnLiteNodePasses() throws Exception { + // index beyond the (empty) genesis body yields null, not 4444 + Assert.assertNull(newRpc(true).getTransactionByBlockNumberAndIndex("0x0", "0x0")); + } + + @Test + public void testFutureBlockOnLiteNodeReturnsNullNotPruned() throws Exception { + // above the cutoff and simply not produced yet: null, never 4444 + Assert.assertNull(newRpc(true).ethGetBlockByNumber("0x7fffffff", false)); + } + + @Test + public void testGetBlockTransactionCountBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockTransactionCountByNumber(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0")); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(BODY_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetLogsFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testNewFilterFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.newFilter(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testNewFilterEarliestOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("earliest", "latest", null, null, null); + + String filterId = newRpc(true).newFilter(fr); + + Assert.assertNotNull(filterId); + Assert.assertTrue(filterId.startsWith("0x")); + } + + @Test + public void testLogFilterGenesisOnlyRangeOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("0x0", "0x0", null, null, null); + + LogFilterWrapper wrapper = + new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + Assert.assertEquals(0L, wrapper.getToBlock()); + } + + @Test + public void testLogFilterGenesisBlockHashOnLiteNodePasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockById(org.mockito.ArgumentMatchers.any())) + .thenReturn(newBlock(0L, 0)); + FilterRequest fr = new FilterRequest(null, null, null, null, + "0x" + Strings.repeat("00", 32)); + + LogFilterWrapper wrapper = new LogFilterWrapper(fr, HEAD_BLOCK_NUM, wallet, false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + } + + @Test + public void testLogFilterEarliestWithLowToBlockIsInvalidRange() { + FilterRequest fr = new FilterRequest("earliest", "0x5", null, null, null); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false)); + } + + @Test + public void testGetLogsInReceiptGapReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest(IN_RECEIPT_GAP_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsInReceiptGapReturns4444() { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertEquals(RECEIPT_FLOOR_HEX, e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockInReceiptGapPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(TransactionInfoList.getDefaultInstance()); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertNotNull(rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + } + + @Test + public void testGetBlockReceiptsAtReceiptFloorPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(TransactionInfoList.getDefaultInstance()); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + } + + @Test + public void testGetBlockTransactionCountInReceiptGapPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertEquals("0x1", rpc.ethGetBlockTransactionCountByNumber(IN_RECEIPT_GAP_HEX)); + } + + @Test + public void testParseBlockTagEarliestWithHistoryOffFallsBackToBodyFloor() throws Exception { + Assert.assertEquals(LOWEST_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newHistoryOffMockWallet())); + } + + @Test + public void testGetBlockByNumberEarliestWithHistoryOffPasses() throws Exception { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + + Assert.assertNull(rpc.ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetLogsWithHistoryOffReturns4444() { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet()); + FilterRequest fr = new FilterRequest(RECEIPT_FLOOR_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() { + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> JsonRpcApiUtil.checkPrunedReceiptHistory(Long.MAX_VALUE, newHistoryOffMockWallet())); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockWithHistoryOffPasses() throws Exception { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(TransactionInfoList.getDefaultInstance()); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + } + + @Test + public void testGetLogsOnFullNodeWithHistoryOffReturns4444() { + // receipt persistence is a per-node switch, so the LiteNode gate must not shield a FullNode + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false)); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsOnFullNodeWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(false); + when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM)) + .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockByNumberOnFullNodeWithHistoryOffPasses() throws Exception { + // bodies are unaffected by the receipt switch + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false)); + + Assert.assertNull(rpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 49f875f3823..06f57750fbe 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -20,6 +20,7 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Commons; +import org.tron.core.exception.jsonrpc.JsonRpcException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.jsonrpc.JsonRpcApiUtil; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; @@ -408,7 +409,7 @@ public void testGetConditions() { Assert.assertArrayEquals(conditions[2][4], getBloomIndex("0x00000000000000000000000056178a0d5f301baf6cf3e1cd53d9863437345bf9")); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } } @@ -448,7 +449,7 @@ public void testGetConditionWithHashCollision() { Assert.assertArrayEquals(conditions[0][1], getBloomIndex("0x3038114c1a1e72c5bfa8b003bc3650ad2ba254a0")); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index e8d14ace060..efdcfc669d2 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -46,6 +46,7 @@ import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.capsule.utils.BlockUtil; import org.tron.core.config.args.Args; +import org.tron.core.exception.jsonrpc.JsonRpcException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.NodeInfoService; @@ -825,7 +826,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, null, null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -835,7 +836,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest("0x14", null, null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -845,7 +846,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest("0x78", null, null, null, null), 100, null, false); Assert.assertEquals(120, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -855,7 +856,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, "0x14", null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(20, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -865,7 +866,7 @@ public void testLogFilterWrapper() { new LogFilterWrapper(new FilterRequest(null, "0x78", null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(120, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -875,7 +876,7 @@ public void testLogFilterWrapper() { null, null, null), 100, null, false); Assert.assertEquals(20, logFilterWrapper.getFromBlock()); Assert.assertEquals(120, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException fromToEx = @@ -887,10 +888,10 @@ public void testLogFilterWrapper() { //fromBlock or toBlock is not hex num try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper(new FilterRequest("earliest", null, - null, null, null), 100, null, false); + null, null, null), 100, wallet, false); Assert.assertEquals(0, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -898,7 +899,7 @@ public void testLogFilterWrapper() { null, null, null), 100, null, false); Assert.assertEquals(100, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException pendingFilterEx = Assert.assertThrows( @@ -911,7 +912,7 @@ public void testLogFilterWrapper() { null, null, null), 100, wallet, false); Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, logFilterWrapper.getFromBlock()); Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock()); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } JsonRpcInvalidParamsException testSyntaxEx = Assert.assertThrows( @@ -924,7 +925,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -939,7 +940,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -955,7 +956,7 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x64", "latest", null, null, null), 5_000, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -997,13 +998,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("latest", "latest", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("latest", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1012,13 +1013,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1026,13 +1027,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1040,13 +1041,13 @@ public void testLogFilterWrapper() { try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, true); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, null, null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1101,7 +1102,7 @@ public void testMaxSubTopics() { tronJsonRpc.getLogs(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), e.getMessage()); @@ -1113,7 +1114,7 @@ public void testMaxSubTopics() { tronJsonRpc.newFilter(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), e.getMessage()); @@ -1126,7 +1127,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } @@ -1134,7 +1135,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -1148,7 +1149,7 @@ public void testMaxSubTopics() { try { new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } try { @@ -1167,7 +1168,7 @@ public void testMethodBlockRange() { tronJsonRpc.getLogs(new FilterRequest("0x0", "0x1f40", null, null, null)); Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.assertEquals( "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, e.getMessage()); @@ -1375,28 +1376,28 @@ public void testGetBlockReceipts() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), transactionReceipt1.getLogs()[0].getBlockTimestamp()); } - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("earliest"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("latest"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("finalized"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1415,7 +1416,7 @@ public void testGetBlockReceipts() { try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1429,7 +1430,7 @@ public void testGetBlockReceipts() { Assert.assertFalse(transactionReceiptList.isEmpty()); Assert.assertEquals(JSON.toJSONString(transactionReceiptList), JSON.toJSONString(transactionReceiptList2)); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java index 77f869fd5a8..5ae31e57110 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java @@ -76,7 +76,7 @@ private Manager buildMockManager(long block1, TransactionInfoList txList1, } private LogMatch buildLogMatch(List blockNums, Manager manager) - throws JsonRpcInvalidParamsException { + throws Exception { FilterRequest fr = new FilterRequest(); // match-all filter LogFilterWrapper wrapper = new LogFilterWrapper(fr, 0L, null, false); return new LogMatch(wrapper, blockNums, manager); @@ -85,8 +85,7 @@ private LogMatch buildLogMatch(List blockNums, Manager manager) /** Under the limit: all logs returned without exception. */ @Test public void testUnderLimit_returnsAllResults() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { int logCount = MAX_RESULT / 2; // 5000, well under limit Manager manager = buildMockManager(100L, buildTxList(logCount)); LogMatch logMatch = buildLogMatch(Collections.singletonList(100L), manager); @@ -101,8 +100,7 @@ public void testUnderLimit_returnsAllResults() */ @Test public void testAtExactLimit_succeeds() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 1 log → total == MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -119,7 +117,7 @@ public void testAtExactLimit_succeeds() */ @Test public void testExceedsLimit_throws() - throws ItemNotFoundException, JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 2 logs → 9999 + 2 = 10001 > MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -132,8 +130,7 @@ public void testExceedsLimit_throws() /** A block with no matching logs is skipped without incrementing the result count. */ @Test public void testEmptyBlockSkipped() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: no logs (empty txInfoList → skipped), block 2: 3 logs Manager manager = mock(Manager.class); ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java index 39bcc30e278..3b7c796fbb0 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java @@ -15,6 +15,7 @@ import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.config.args.Args; import org.tron.core.exception.EventBloomException; @@ -30,6 +31,9 @@ public class SectionBloomStoreTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + @Resource + private Wallet wallet; + private ExecutorService sectionExecutor; static { @@ -145,7 +149,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", ByteArray.toJsonHex(address1), null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -162,7 +166,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", addressList, null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -178,7 +182,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic1)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -194,7 +198,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -212,7 +216,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {topicList}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -239,7 +243,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {ByteArray.toJsonHex(topic1), ByteArray.toJsonHex(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java index d8e64308ab8..5fe007e27dc 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java @@ -11,6 +11,7 @@ import org.junit.Assert; import org.junit.Test; import org.tron.core.exception.jsonrpc.JsonRpcException; +import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException; import org.tron.core.exception.jsonrpc.JsonRpcInternalException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; @@ -22,6 +23,7 @@ public class JsonRpcErrorResolverTest { @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), @JsonRpcError(exception = JsonRpcException.class, code = -1) }) @@ -61,6 +63,16 @@ public void testResolveErrorWithTronException() throws Exception { Assert.assertEquals(message, error.message); Assert.assertEquals("{}", error.data); + message = "execution reverted"; + data = "0x"; + exception = new JsonRpcExecutionRevertedException(message, data); + error = resolver.resolveError(exception, method, arguments); + + Assert.assertNotNull(error); + Assert.assertEquals(3, error.code); + Assert.assertEquals(message, error.message); + Assert.assertEquals(data, error.data); + message = "JsonRpcException"; exception = new JsonRpcException(message, null); error = resolver.resolveError(exception, method, arguments); @@ -72,4 +84,28 @@ public void testResolveErrorWithTronException() throws Exception { } -} \ No newline at end of file + @Test + public void testAnnotationOrderNeverShadowsSubclassCodes() { + for (Method method : TronJsonRpc.class.getMethods()) { + JsonRpcErrors errors = method.getAnnotation(JsonRpcErrors.class); + if (errors == null) { + continue; + } + JsonRpcError[] entries = errors.value(); + for (int earlier = 0; earlier < entries.length; earlier++) { + for (int later = earlier + 1; later < entries.length; later++) { + Class earlierEx = entries[earlier].exception(); + Class laterEx = entries[later].exception(); + Assert.assertFalse(String.format( + "%s.%s: @JsonRpcError for %s (code %d) is unreachable — its superclass %s " + + "(code %d) is declared before it; move the subclass entry up", + TronJsonRpc.class.getSimpleName(), method.getName(), + laterEx.getSimpleName(), entries[later].code(), + earlierEx.getSimpleName(), entries[earlier].code()), + earlierEx != laterEx && earlierEx.isAssignableFrom(laterEx)); + } + } + } + } + +} diff --git a/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java new file mode 100644 index 00000000000..266562c5eea --- /dev/null +++ b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java @@ -0,0 +1,66 @@ +package org.tron.core.utils; + +import java.lang.reflect.Method; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.runtime.ProgramResult; +import org.tron.common.runtime.RuntimeImpl; +import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.Program.BadJumpDestinationException; +import org.tron.core.vm.program.Program.OutOfEnergyException; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.protos.Protocol.Transaction.Result.contractResult; + +public class ResultCodeUtilTest { + + @Test + public void testTypedExceptionsResolveToTheirCodes() { + Assert.assertEquals(contractResult.OUT_OF_ENERGY, + ResultCodeUtil.resolve(new OutOfEnergyException("out of energy"))); + Assert.assertEquals(contractResult.OUT_OF_TIME, + ResultCodeUtil.resolve(new OutOfTimeException("out of time"))); + Assert.assertEquals(contractResult.BAD_JUMP_DESTINATION, + ResultCodeUtil.resolve(new BadJumpDestinationException("bad jump"))); + } + + @Test + public void testUntypedExceptionResolvesToUnknown() { + Assert.assertEquals(contractResult.UNKNOWN, + ResultCodeUtil.resolve(new RuntimeException("untyped failure"))); + } + + @Test + public void testNullExceptionResolvesToUnknown() { + Assert.assertEquals(contractResult.UNKNOWN, ResultCodeUtil.resolve(null)); + } + + @Test + public void testStaysInSyncWithConsensusClassifier() throws Exception { + RuntimeException[] samples = { + new Program.IllegalOperationException("op"), + new Program.OutOfEnergyException("energy"), + new Program.BadJumpDestinationException("jump"), + new Program.OutOfTimeException("time"), + new Program.OutOfMemoryException("mem"), + new Program.PrecompiledContractException("pre"), + new Program.StackTooSmallException("small"), + new Program.JVMStackOverFlowException(), + new Program.TransferException("transfer"), + new Program.InvalidCodeException("code"), + new Program.StaticCallModificationException(), + new RuntimeException("untyped"), + }; + + Method consensus = RuntimeImpl.class.getDeclaredMethod("setResultCode", ProgramResult.class); + consensus.setAccessible(true); + RuntimeImpl runtime = new RuntimeImpl(); + + for (RuntimeException e : samples) { + ProgramResult result = new ProgramResult(); + result.setException(e); + consensus.invoke(runtime, result); + Assert.assertEquals("classifiers diverged for " + e.getClass().getSimpleName(), + result.getResultCode(), ResultCodeUtil.resolve(e)); + } + } +}