diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 21f0bac8d77..93e149bc472 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; @@ -394,6 +401,15 @@ 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; + // Probed from the store itself, not from snapshot metadata; empty store falls back to + // the head. This runs before checkpoint recovery, so the very first session can be one + // block conservative; restarts self-correct. With receipt persistence off the store + // never grows, so no floor exists — receipt history is simply unavailable. + boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() + .getStorage().getTransactionHistorySwitch()); + this.lowestReceiptBlockNum = persistReceipts + ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1) + : Long.MAX_VALUE; this.latestSaveBlockTime = System.currentTimeMillis(); } 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/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index eeb92fdbd60..8337e545463 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -490,6 +490,9 @@ public class CommonParameter { public int jsonRpcMaxLogFilterNum = 20000; @Getter @Setter + public boolean jsonRpcStrictComplianceMode = false; + @Getter + @Setter public int maxTransactionPendingSize; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 2158f56d0ba..1f66b995590 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -247,6 +247,7 @@ public static class JsonRpcConfig { private int maxAddressSize = 1000; private int maxLogFilterNum = 20000; private long maxMessageSize = 4194304; + private boolean strictComplianceMode = false; } @Getter 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..6c592ea8453 --- /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 JsonRpcInternalException { + + 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..4198118f638 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java @@ -0,0 +1,13 @@ +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 JsonRpcInternalException { + + public JsonRpcPrunedHistoryException(String message) { + super(message); + } +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 25fc4832e55..bc93372181e 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -441,6 +441,8 @@ node { maxLogFilterNum = 20000 # Maximum JSON-RPC request body size in bytes (default 4194304, ~4MB). Independent from rpc.maxMessageSize. maxMessageSize = 4194304 + # Reject requests that violate JSON-RPC 2.0. + strictComplianceMode = false } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. 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/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0bca242606e..f2a0b995081 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -560,6 +560,7 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize(); PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum(); PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize(); + PARAMETER.jsonRpcStrictComplianceMode = jsonrpc.isStrictComplianceMode(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); 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..594507cd277 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,13 @@ public static long parseBlockTag(String tag, Wallet wallet) return wallet.getHeadBlockNum(); } if (EARLIEST_STR.equalsIgnoreCase(tag)) { - return 0; + if (!wallet.isLiteNode()) { + return 0; + } + // "earliest" anchors to the receipt floor (first block with complete data); with + // receipt persistence off no such block exists — fall back to the body floor + long receiptFloor = wallet.getLowestReceiptBlockNum(); + return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor; } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -700,6 +708,42 @@ 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 new JsonRpcPrunedHistoryException(prunedMessage(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. + */ + public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + if (!wallet.isLiteNode()) { + return; + } + long receiptFloor = wallet.getLowestReceiptBlockNum(); + if (receiptFloor == Long.MAX_VALUE) { + throw new JsonRpcPrunedHistoryException( + PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node"); + } + if (blockNum < receiptFloor) { + throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor)); + } + } + + private static String prunedMessage(long earliestAvailable) { + return PRUNED_HISTORY_ERROR + ": earliest available block is 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/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index ca249da4e5d..e60f59b2ba0 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -133,6 +133,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); return; } + if (!isBatch && violatesStrictCompliance(rootNode)) { + JsonNode id = rootNode.get("id"); + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", + id != null && (id.isTextual() || id.isNumber()) ? id : null, false); + return; + } int batchSize = parameter.getJsonRpcMaxBatchSize(); if (isBatch && batchSize > 0 && rootNode.size() > batchSize) { writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, @@ -193,8 +199,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes continue; } - if (!subRequest.isObject()) { - ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null); + if (!subRequest.isObject() || violatesStrictCompliance(subRequest)) { + JsonNode subId = subRequest.get("id"); + ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", + subId != null && (subId.isTextual() || subId.isNumber()) ? subId : null); byte[] errBytes = MAPPER.writeValueAsBytes(errNode); int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0); if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { @@ -265,6 +273,36 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes resp.getOutputStream().flush(); } + /** + * Whether a request object breaks JSON-RPC 2.0 (https://www.jsonrpc.org/specification), + * checked only when {@code node.jsonrpc.strictComplianceMode} is on. jsonrpc4j is tolerant + * by default: it accepts a missing or wrong {@code jsonrpc} field and any {@code id} type. + */ + private static boolean violatesStrictCompliance(JsonNode node) { + if (!CommonParameter.getInstance().isJsonRpcStrictComplianceMode()) { + return false; + } + JsonNode version = node.get("jsonrpc"); + if (version == null || !version.isTextual() || !"2.0".equals(version.asText())) { + return true; + } + // A malformed request object is -32600, not the -32601 jsonrpc4j reports for a missing + // or non-string method. + JsonNode method = node.get("method"); + if (method == null || !method.isTextual()) { + return true; + } + // params may be omitted; a present one must be an Array or Object per JSON-RPC 2.0 §4.2 + JsonNode params = node.get("params"); + if (params != null && !params.isArray() && !params.isObject()) { + return true; + } + // Any non-scalar id is rejected. Spec deviation: null is a valid id per JSON-RPC 2.0, + // but jsonrpc4j treats it as a notification and answers nothing, so -32600 beats silence. + JsonNode id = node.get("id"); + return id != null && !id.isTextual() && !id.isNumber(); + } + private byte[] readBody(InputStream in) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] tmp = new byte[4096]; 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..5104e213788 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,6 +125,7 @@ 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, @@ -141,9 +147,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,6 +161,7 @@ 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) @@ -163,6 +171,7 @@ List getBlockReceipts(String blockNumOrHashOrTag) @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) @@ -292,9 +301,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 +337,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 +345,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..67b74b5ca90 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); + } + // 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(parseBlockNumber(blockNumOrTag)); + return wallet.getBlockByNum(blockNum); } private BlockResult getBlockResult(Block block, boolean fullTx) { @@ -543,6 +556,25 @@ 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 { + 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 @@ -577,27 +609,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 @@ -730,25 +749,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 +850,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; @@ -947,13 +953,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: " @@ -1438,7 +1449,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 +1549,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/main/resources/config.conf b/framework/src/main/resources/config.conf index 1176dd46311..fbdafc78310 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -163,6 +163,9 @@ node { maxResponseSize = 26214400 maxLogFilterNum = 20000 maxMessageSize = 4194304 + + # Reject requests that violate JSON-RPC 2.0. + # strictComplianceMode = false } disabledApi = [ 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..dfa1614c7f1 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,31 @@ 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 get() throws BadItemException { TransactionInfoCapsule resultCapsule = transactionRetStore.getTransactionInfo(transactionId); 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..4cf6f76032e 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,31 @@ 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.assertFalse(e instanceof JsonRpcExecutionRevertedException); + 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.assertFalse(e instanceof JsonRpcExecutionRevertedException); + Assert.assertEquals("Unknown failure", e.getMessage()); + Assert.assertEquals("0xdeadbeef00", e.getData()); + } + @Test public void testGetCallReturnsConstantResult() throws Exception { byte[] part1 = ByteArray.fromHexString("deadbeef"); @@ -173,8 +215,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 +225,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 +238,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 +248,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..1ad94adee5a --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -0,0 +1,349 @@ +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 BODY_PRUNED_MESSAGE = + "Pruned history unavailable: earliest available block is 0x64"; + private static final String RECEIPT_PRUNED_MESSAGE = + "Pruned history unavailable: earliest available block is 0x96"; + private static final String HISTORY_OFF_PRUNED_MESSAGE = + "Pruned history unavailable: transaction history is not persisted on this node"; + 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() { + Wallet wallet = newMockWallet(true); + 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(BODY_PRUNED_MESSAGE, e.getMessage()); + } + + @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(BODY_PRUNED_MESSAGE, e.getMessage()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0")); + Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage()); + } + + @Test + public void testGetBlockReceiptsBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage()); + } + + @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(RECEIPT_PRUNED_MESSAGE, e.getMessage()); + } + + @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(RECEIPT_PRUNED_MESSAGE, e.getMessage()); + } + + @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(RECEIPT_PRUNED_MESSAGE, e.getMessage()); + } + + @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(RECEIPT_PRUNED_MESSAGE, e.getMessage()); + } + + @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(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage()); + } + + @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(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage()); + } + + @Test + public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() { + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> JsonRpcApiUtil.checkPrunedReceiptHistory(Long.MAX_VALUE, newHistoryOffMockWallet())); + Assert.assertEquals(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage()); + } + + @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)); + } +} 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..21d62a4f1fc 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()); 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/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index d6c843b5aea..ae920c5f2d1 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -36,6 +36,7 @@ public class JsonRpcServletTest { private JsonRpcServer mockRpcServer; private int savedMaxBatchSize; private int savedMaxResponseSize; + private boolean savedStrictComplianceMode; @Before public void setUp() throws Exception { @@ -46,12 +47,14 @@ public void setUp() throws Exception { f.set(servlet, mockRpcServer); savedMaxBatchSize = CommonParameter.getInstance().jsonRpcMaxBatchSize; savedMaxResponseSize = CommonParameter.getInstance().jsonRpcMaxResponseSize; + savedStrictComplianceMode = CommonParameter.getInstance().jsonRpcStrictComplianceMode; } @After public void tearDown() { CommonParameter.getInstance().jsonRpcMaxBatchSize = savedMaxBatchSize; CommonParameter.getInstance().jsonRpcMaxResponseSize = savedMaxResponseSize; + CommonParameter.getInstance().jsonRpcStrictComplianceMode = savedStrictComplianceMode; } // --- parse error paths --- @@ -355,12 +358,12 @@ public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws E @Test public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception { - MockHttpServletResponse resp = doPost("[42, \"foo\", true]"); + MockHttpServletResponse resp = doPost("[42, \"foo\", true, null]"); assertEquals(200, resp.getStatus()); JsonNode body = MAPPER.readTree(resp.getContentAsString()); assertTrue("response must be a JSON array", body.isArray()); - assertEquals(3, body.size()); - for (int i = 0; i < 3; i++) { + assertEquals(4, body.size()); + for (int i = 0; i < body.size(); i++) { assertEquals(-32600, body.get(i).get("error").get("code").asInt()); } } @@ -417,6 +420,181 @@ public void tooManyTokens_returnsParseError() throws Exception { // --- helpers --- + // --- strict compliance mode --- + + @Test + public void strictModeOff_tolerantOfMissingVersionAndOddId() throws Exception { + stubSingleOkResponse(); + + MockHttpServletResponse resp = doPost("{\"method\":\"eth_chainId\",\"id\":[1,2]}"); + + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals("default behaviour must stay tolerant", "ok", body.get("result").asText()); + } + + @Test + public void strictModeOn_missingVersionIsInvalidRequest() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost("{\"method\":\"eth_chainId\",\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals("a scalar id is echoed back", 1, body.get("id").asInt()); + } + + @Test + public void strictModeOn_wrongVersionIsInvalidRequest() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"1.0\",\"method\":\"eth_chainId\",\"id\":\"a\"}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals("a", body.get("id").asText()); + } + + @Test + public void strictModeOn_structuredIdIsInvalidRequestWithNullId() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":{\"a\":1}}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue("spec §5: an undeterminable id must be null", body.get("id").isNull()); + } + + @Test + public void strictModeOn_missingMethodIsInvalidRequest() throws Exception { + // jsonrpc4j reports -32601 for these, but a malformed request object is -32600 + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertEquals(1, body.get("id").asInt()); + } + + @Test + public void strictModeOn_nonStringMethodIsInvalidRequest() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"method\":123,\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + } + + @Test + public void strictModeOn_nonStructuredParamsIsInvalidRequest() throws Exception { + // without the check jsonrpc4j answers nothing at all and the caller hangs + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"params\":\"nope\",\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + } + + @Test + public void strictModeOn_nullParamsIsInvalidRequest() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"params\":null,\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + } + + @Test + public void strictModeOn_omittedParamsIsAccepted() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + stubSingleOkResponse(); + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals("params may be omitted", "ok", body.get("result").asText()); + } + + @Test + public void strictModeOn_compliantRequestStillRuns() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + stubSingleOkResponse(); + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":1}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals("ok", body.get("result").asText()); + } + + @Test + public void strictModeOn_explicitNullIdIsRejectedInsteadOfHanging() throws Exception { + // jsonrpc4j 1.6 turns an explicit null id into a notification (parseId -> null -> + // isNotificationRequest) and writes no response at all, so the client would wait forever. + // Strict mode answers -32600 instead. + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":null}"); + + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue(body.get("id").isNull()); + } + + @Test + public void strictModeOff_explicitNullIdKeepsLegacyNotificationBehaviour() throws Exception { + // Unchanged when strict mode is off: the request reaches jsonrpc4j as before. + MockHttpServletResponse resp = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":null}"); + + assertEquals(200, resp.getStatus()); + assertEquals("no servlet-level rejection when strict mode is off", + 0, resp.getContentAsByteArray().length); + } + + @Test + public void strictModeOn_batchValidatesEachMemberSeparately() throws Exception { + CommonParameter.getInstance().jsonRpcStrictComplianceMode = true; + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":2}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_chainId\",\"id\":1}," + + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":2}]"); + + JsonNode body = MAPPER.readTree(resp.getContentAsByteArray()); + assertTrue(body.isArray()); + assertEquals(2, body.size()); + assertEquals("non-compliant member is rejected on its own", + -32600, body.get(0).get("error").get("code").asInt()); + assertEquals("compliant member still executes", "ok", body.get(1).get("result").asText()); + } + + private void stubSingleOkResponse() throws Exception { + doAnswer(inv -> { + HttpServletResponse r = inv.getArgument(1); + byte[] out = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + r.getOutputStream().write(out); + return null; + }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + private MockHttpServletResponse doPost(String body) throws Exception { MockHttpServletRequest req = new MockHttpServletRequest("POST", "/jsonrpc"); req.setContent(body.getBytes(StandardCharsets.UTF_8)); 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)); + } + } +}