diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java index 21f0bac8d77..85b739cd721 100644 --- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java +++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java @@ -9,8 +9,10 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.metric.DbStatService; import org.tron.common.utils.ForkController; import org.tron.common.utils.Sha256Hash; @@ -244,10 +246,38 @@ public class ChainBaseManager { @Setter private long lowestBlockNum = -1; // except num = 0. + // first block of transactionRetStore; head + 1 when the store is empty, + // Long.MAX_VALUE when receipts are not persisted + @Getter + private long lowestBlockNumOfReceiptStore = -1; + @Getter @Setter private long latestSaveBlockTime; + @PostConstruct + private void init() { + this.lowestBlockNum = this.blockIndexStore.getLimitNumber(1, 1).stream() + .map(BlockId::getNum).findFirst().orElse(0L); + this.nodeType = getLowestBlockNum() > 1 ? NodeType.LITE : NodeType.FULL; + this.latestSaveBlockTime = System.currentTimeMillis(); + } + + /** + * Reads the lowest receipt block from the store itself, not from snapshot metadata; an empty + * store means receipts begin with the next executed block. With receipt persistence off + * the store never grows, so no lower bound exists. Must run after checkpoint recovery (so the + * last session's tail is visible) and before any session is built ({@code getNext} does + * not merge in-flight layers). + */ + public void initLowestBlockNumOfReceiptStore() { + boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance() + .getStorage().getTransactionHistorySwitch()); + this.lowestBlockNumOfReceiptStore = persistReceipts + ? this.transactionRetStore.getLowestReceiptBlockNum().orElseGet(() -> getHeadBlockNum() + 1) + : Long.MAX_VALUE; + } + // for test only public List getWitnesses() { return witnessScheduleStore.getActiveWitnesses(); @@ -389,14 +419,6 @@ public long getNextBlockSlotTime() { + slotCount * BLOCK_PRODUCED_INTERVAL; } - @PostConstruct - private void init() { - this.lowestBlockNum = this.blockIndexStore.getLimitNumber(1, 1).stream() - .map(BlockId::getNum).findFirst().orElse(0L); - this.nodeType = getLowestBlockNum() > 1 ? NodeType.LITE : NodeType.FULL; - this.latestSaveBlockTime = System.currentTimeMillis(); - } - public void shutdown() { dbStatService.shutdown(); } diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java index 8ff3064b73c..2f1b1ee80b6 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java @@ -147,6 +147,10 @@ public void setErrorCode(Result.code code) { this.transactionResult = this.transactionResult.toBuilder().setRet(code).build(); } + public void setResultCode(contractResult code) { + this.transactionResult = this.transactionResult.toBuilder().setContractRet(code).build(); + } + public long getShieldedTransactionFee() { return transactionResult.getShieldedTransactionFee(); } @@ -184,4 +188,4 @@ public byte[] getData() { public Result getInstance() { return this.transactionResult; } -} \ No newline at end of file +} diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java index a22e69e8692..b316bbb2546 100644 --- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java @@ -1,7 +1,9 @@ package org.tron.core.store; +import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; import java.util.Objects; +import java.util.OptionalLong; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +39,18 @@ public void put(byte[] key, TransactionRetCapsule item) { } } + /** + * Lowest block number that has receipts, or empty when the store has none. On a LiteNode + * this is generally above the lowest block: a snapshot ships block bodies but no receipts. + * + *

Startup use only — must run before any session is built. With in-flight snapshot + * layers, {@code getNext} does not merge deletions correctly. + */ + public OptionalLong getLowestReceiptBlockNum() { + return revokingDB.getNext(ByteArray.fromLong(0), 1).keySet().stream() + .mapToLong(Longs::fromByteArray).findFirst(); + } + public TransactionInfoCapsule getTransactionInfo(byte[] key) throws BadItemException { long blockNumber = transactionStore.getBlockNumber(key); if (blockNumber == -1) { diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java new file mode 100644 index 00000000000..5a090e76705 --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java @@ -0,0 +1,12 @@ +package org.tron.core.exception.jsonrpc; + +public class JsonRpcExecutionRevertedException extends JsonRpcException { + + public JsonRpcExecutionRevertedException(String message) { + super(message); + } + + public JsonRpcExecutionRevertedException(String message, Object data) { + super(message, data); + } +} diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java new file mode 100644 index 00000000000..259081b317b --- /dev/null +++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java @@ -0,0 +1,17 @@ +package org.tron.core.exception.jsonrpc; + +/** + * Thrown when a request targets data the node does not have: history pruned on a LiteNode, + * or receipts and logs on a node that does not persist them. Maps to JSON-RPC error code 4444 + * "Pruned history unavailable", as standardized by the Ethereum Execution API (EIP-4444). + */ +public class JsonRpcPrunedHistoryException extends JsonRpcException { + + public JsonRpcPrunedHistoryException(String message) { + super(message); + } + + public JsonRpcPrunedHistoryException(String message, Object data) { + super(message, data); + } +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 28228e38a08..bcece070243 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -47,7 +47,10 @@ storage { db.directory = "database" # Database directory under the node --output-directory path. - # Whether to write transaction result in transactionRetStore + # Whether to store transaction receipts (fee, energy usage, logs) in transactionRetStore. + # When "off", the JSON-RPC methods eth_getBlockReceipts, eth_getLogs and eth_newFilter + # return error 4444, including for blocks whose receipts were stored while the switch + # was "on". Queries for the genesis block alone are still answered. transHistory.switch = "on" # Per-database LevelDB option overrides.Default: empty. All databases use global LevelDB settings. diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 913d8578216..fa394d4a905 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -236,6 +236,7 @@ import org.tron.protos.Protocol.Transaction.Contract; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; import org.tron.protos.contract.BalanceContract; @@ -743,6 +744,18 @@ public long getHeadBlockNum() { return chainBaseManager.getHeadBlockNum(); } + public boolean isLiteNode() { + return chainBaseManager.isLiteNode(); + } + + public long getLowestBlockNum() { + return chainBaseManager.getLowestBlockNum(); + } + + public long getLowestBlockNumOfReceiptStore() { + return chainBaseManager.getLowestBlockNumOfReceiptStore(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -3208,6 +3221,7 @@ public Transaction callConstantContract(TransactionCapsule trxCap, } if (result.isRevert()) { ret.setStatus(0, code.FAILED); + ret.setResultCode(contractResult.REVERT); retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed")) .build(); } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index b9c94258fb3..0efa48dc20a 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -461,6 +461,7 @@ public void init() { trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); revokingStore.check(); + chainBaseManager.initLowestBlockNumOfReceiptStore(); transactionCache.initCache(); rewardViCalService.init(); this.setProposalController(ProposalController.createInstance(this)); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index bd13f998112..05aaedb2bc6 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 @@ -23,6 +23,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; @@ -61,6 +62,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(); @@ -633,7 +635,7 @@ public static long parseBlockTag(String tag, Wallet wallet) return wallet.getHeadBlockNum(); } if (EARLIEST_STR.equalsIgnoreCase(tag)) { - return 0; + return resolveEarliestBlockNum(wallet); } if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); @@ -697,6 +699,47 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) return parseBlockNumber(blockNumOrTag); } + /** + * The lowest block for which everything the node persists is available; what "earliest" + * resolves to. On a LiteNode this is the + * lowest receipt block when receipts are persisted, otherwise the lowest block (receipt endpoints + * answer 4444 on such a node regardless of this value). On a FullNode it is genesis. + */ + public static long resolveEarliestBlockNum(Wallet wallet) { + if (!wallet.isLiteNode()) { + return 0; + } + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + return lowestReceiptBlockNum == Long.MAX_VALUE + ? wallet.getLowestBlockNum() : lowestReceiptBlockNum; + } + + /** + * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444. + * Raw primitive — no genesis exemption; callers own that semantics. + */ + public static void checkPrunedBlockHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + } + + /** + * Receipt form of {@link #checkPrunedBlockHistory(long, Wallet)} for endpoints that read + * receipts or logs; their lower bound is the lowest receipt block. Same raw-primitive + * contract. Receipt persistence is a per-node switch independent of node type, so a node + * that never persists receipts is rejected before the LiteNode gate. + */ + public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) + throws JsonRpcPrunedHistoryException { + long lowestReceiptBlockNum = wallet.getLowestBlockNumOfReceiptStore(); + if (lowestReceiptBlockNum == Long.MAX_VALUE + || (wallet.isLiteNode() && blockNum < lowestReceiptBlockNum)) { + throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR); + } + } + /** * Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the * longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix. diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 7fd5dcd450e..2feb4d485ea 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 @@ -4,7 +4,6 @@ import com.googlecode.jsonrpc4j.JsonRpcError; import com.googlecode.jsonrpc4j.JsonRpcErrors; import com.googlecode.jsonrpc4j.JsonRpcMethod; -import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; @@ -19,10 +18,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 +56,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 +71,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") @JsonRpcErrors({ @@ -128,10 +132,12 @@ String getABIOfSmartContract(String contractAddress, String bnOrId) @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException; + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException; @JsonRpcMethod("eth_getTransactionByHash") @JsonRpcErrors({ @@ -149,9 +155,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({ @@ -162,20 +169,23 @@ TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, Stri @JsonRpcMethod("eth_getBlockReceipts") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}") }) List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException; + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException; @JsonRpcMethod("eth_call") @JsonRpcErrors({ @JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"), @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"), @JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"), }) String getCall(CallArguments transactionCall, Object blockNumOrTag) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, - JsonRpcInternalException; + JsonRpcInternalException, JsonRpcExecutionRevertedException; @JsonRpcMethod("net_peerCount") String getPeerCount(); @@ -300,9 +310,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({ @@ -326,14 +337,14 @@ boolean uninstallFilter(String filterId) throws JsonRpcInvalidParamsException, @JsonRpcError(exception = ItemNotFoundException.class, code = -32000, data = "{}"), }) Object[] getFilterChanges(String filterId) - throws JsonRpcInvalidParamsException, IOException, ExecutionException, InterruptedException, - JsonRpcMethodNotFoundException, ItemNotFoundException; + throws JsonRpcInvalidParamsException, JsonRpcMethodNotFoundException, ItemNotFoundException; @JsonRpcMethod("eth_getLogs") @JsonRpcErrors({ @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, message = "Internal error", data = "{}"), @@ -343,7 +354,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 f39e5a1a2d4..a40e22bafaa 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 @@ -75,10 +75,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.servlets.JsonFormat; @@ -105,6 +107,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; @@ -385,7 +388,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; @@ -406,7 +409,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { Long blockNum = parseBlockSelector(blockNumOrTag); requireParam(fullTransactionObjects, INVALID_PARAMS); final Block b = getBlockBySelector(blockNum); @@ -445,7 +448,8 @@ private Block getBlockByHash(byte[] blockHash) { return wallet.getBlockById(ByteString.copyFrom(blockHash)); } - private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + private Block getBlockByNumOrTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { return getBlockBySelector(parseBlockSelector(blockNumOrTag)); } @@ -462,12 +466,18 @@ private Long parseBlockSelector(String blockNumOrTag) throws JsonRpcInvalidParam return parseBlockNumber(blockNumOrTag); } - private Block getBlockBySelector(Long blockNum) { + private Block getBlockBySelector(Long blockNum) throws JsonRpcPrunedHistoryException { if (blockNum == null) { // Return the head block directly from blockStore, bypassing blockIndexStore // which may not yet be written when latestBlockHeaderNumber is already updated. return wallet.getNowBlock(); } + // Reject a pruned height before touching any store, so a LiteNode pays no lookup for + // history it cannot serve. Genesis is exempt: a snapshot copies block 0 explicitly, and + // lowestBlockNum is computed from block 1 upwards, so block 0 is always retained. + if (blockNum > 0) { + JsonRpcApiUtil.checkPrunedBlockHistory(blockNum, wallet); + } return wallet.getBlockByNum(blockNum); } @@ -613,12 +623,33 @@ static String tryDecodeRevertReason(byte[] resData) { } } + /** + * Rejects a failed constant-call execution: throws code 3 with the revert payload in data + * for a contract revert, -32000 for any other execution failure. + */ + private void requireExecutionSuccess(TransactionExtention.Builder trxExtBuilder, + Return.Builder retBuilder) + throws JsonRpcInternalException, JsonRpcExecutionRevertedException { + Transaction.Result txResult = trxExtBuilder.getTransaction().getRet(0); + if (txResult.getRet().equals(code.SUCESS)) { + return; + } + byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); + if (txResult.getContractRet() == contractResult.REVERT) { + throw new JsonRpcExecutionRevertedException(errMsg, ByteArray.toJsonHex(resData)); + } + throw new JsonRpcInternalException(errMsg, + resData.length > 0 ? ByteArray.toJsonHex(resData) : null); + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 */ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long value, - byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException { + byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); Return.Builder retBuilder = Return.newBuilder(); @@ -647,27 +678,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 @@ -743,7 +761,8 @@ public String gasPrice() { @Override public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException, - JsonRpcInvalidParamsException, JsonRpcInternalException { + JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcExecutionRevertedException { requireParam(args, INVALID_PARAMS); byte[] ownerAddress = addressCompatibleToByteArray(args.getFrom()); @@ -803,25 +822,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 @@ -887,10 +893,7 @@ private TransactionResult formatTransactionResult(TransactionInfo transactioninf energyUsageTotal, wallet.getEnergyFee(blockCapsule.getTimeStamp()), wallet); } - private TransactionResult getTransactionByBlockAndIndex(Block block, String index) - throws JsonRpcInvalidParamsException { - int txIndex = parseTxIndex(index); - + private TransactionResult getTransactionByBlockAndIndex(Block block, int txIndex) { if (txIndex < 0 || txIndex >= block.getTransactionsCount()) { return null; } @@ -906,24 +909,26 @@ private TransactionResult getTransactionByBlockAndIndex(Block block, String inde @Override public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String index) throws JsonRpcInvalidParamsException { + int txIndex = parseTxIndex(index); final Block block = getBlockByJsonHash(blockHash); if (block == null) { return null; } - return getTransactionByBlockAndIndex(block, index); + return getTransactionByBlockAndIndex(block, txIndex); } @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { + int txIndex = parseTxIndex(index); Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } - return getTransactionByBlockAndIndex(block, index); + return getTransactionByBlockAndIndex(block, txIndex); } /** @@ -998,11 +1003,13 @@ private TransactionContext findTransactionContext(TransactionInfoList infoList, * @return List of TransactionReceipt objects for all transactions in the block, * null if block not found * @throws JsonRpcInvalidParamsException if the parameter format is invalid + * @throws JsonRpcPrunedHistoryException if the node does not have the block's receipts * @throws JsonRpcInternalException if there's an internal error */ @Override public List getBlockReceipts(String blockNumOrHashOrTag) - throws JsonRpcInvalidParamsException, JsonRpcInternalException { + throws JsonRpcInvalidParamsException, JsonRpcInternalException, + JsonRpcPrunedHistoryException { Block block = null; @@ -1013,13 +1020,15 @@ public List getBlockReceipts(String blockNumOrHashOrTag) block = getBlockByNumOrTag(blockNumOrHashOrTag); } - // block receipts not available: block is genesis, not produced yet, or pruned in light node + // block receipts not available: block is genesis, not produced yet, or unknown hash if (block == null || block.getBlockHeader().getRawData().getNumber() == 0) { return null; } BlockCapsule blockCapsule = new BlockCapsule(block); long blockNum = blockCapsule.getNum(); + // below the lowest receipt block the body exists but the receipts do not — 4444, not -32000 + JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet); TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum); // energy price at the block timestamp @@ -1073,7 +1082,7 @@ private List getTransactionReceiptsFromBlock(BlockCapsule bl @Override public String getCall(CallArguments transactionCall, Object blockParamObj) throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException, - JsonRpcInternalException { + JsonRpcInternalException, JsonRpcExecutionRevertedException { requireParam(transactionCall, INVALID_PARAMS); String blockNumOrTag; @@ -1515,7 +1524,8 @@ public CompilationResult ethSubmitHashrate(String hashrate, String id) @Override public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException, - JsonRpcMethodNotFoundException, JsonRpcExceedLimitException { + JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, + JsonRpcPrunedHistoryException { disableInPBFT("eth_newFilter"); requireParam(fr, INVALID_FILTER_REQUEST); @@ -1615,7 +1625,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"); requireParam(fr, INVALID_FILTER_REQUEST); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java index 57739819d1e..6c3296e5c0c 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java @@ -6,6 +6,7 @@ import lombok.Getter; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; @@ -15,7 +16,7 @@ public class LogFilterAndResult extends FilterResult { private final LogFilterWrapper logFilterWrapper; public LogFilterAndResult(FilterRequest fr, long currentMaxBlockNum, Wallet wallet) - throws JsonRpcInvalidParamsException { + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // eth_newFilter, no need to check block range this.logFilterWrapper = new LogFilterWrapper(fr, currentMaxBlockNum, wallet, false); result = new LinkedBlockingQueue<>(); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 0fdf174bb50..c45ed8d0915 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -9,6 +9,7 @@ import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; import org.tron.core.services.jsonrpc.JsonRpcApiUtil; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.protos.Protocol.Block; @@ -25,7 +26,8 @@ public class LogFilterWrapper { private final long toBlock; public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet, - boolean checkBlockRange) throws JsonRpcInvalidParamsException { + boolean checkBlockRange) + throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException { // 1.convert FilterRequest to LogFilter this.logFilter = new LogFilter(fr); @@ -103,6 +105,12 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet this.fromBlock = fromBlockSrc; this.toBlock = toBlockSrc; + // Reject a range starting below the lowest receipt block with 4444. Exception: a genesis-only + // query (from = to = 0, or the genesis blockHash) — block 0 is retained. + if (wallet != null && (fromBlockSrc != 0 || toBlockSrc != 0)) { + JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet); + } + // eth_getLogs enforces the block range at construction time. eth_newFilter creates the // wrapper with checkBlockRange=false (no creation-time gate); eth_getFilterLogs re-runs this // check against the current head before scanning so the cap cannot be bypassed. diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 64e70964a6f..1413bf8b6c1 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -8,7 +8,10 @@ storage { db.sync = false, db.directory = "database", - # Whether to write transaction result in transactionRetStore + # Whether to store transaction receipts (fee, energy usage, logs) in transactionRetStore. + # When "off", the JSON-RPC methods eth_getBlockReceipts, eth_getLogs and eth_newFilter + # return error 4444, including for blocks whose receipts were stored while the switch + # was "on". Queries for the genesis block alone are still answered. transHistory.switch = "on", # Per-database storage path overrides (name is required; see reference.conf for full property list). diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index d257f9de5d5..230e3e85f82 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -1403,6 +1403,41 @@ public void testEstimateEnergyOutOfTime() { contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder)); } + @Test + @SneakyThrows + public void testTriggerConstantRevertSetsContractRet() { + String contractAddress = "0x1A622D84ed49f01045f5f1a5AfcEb9c57e9cC3cd"; + + SmartContractOuterClass.SmartContract smartContract = + SmartContractOuterClass.SmartContract.newBuilder().build(); + ContractCapsule capsule = new ContractCapsule(smartContract); + dbManager.getContractStore().put(ByteArray.fromHexString(contractAddress), capsule); + + // PUSH1 0 PUSH1 0 REVERT + CodeCapsule codeCapsule = new CodeCapsule(ByteArray.fromHexString("60006000fd")); + dbManager.getCodeStore().put(ByteArray.fromHexString(contractAddress), codeCapsule); + + SmartContractOuterClass.TriggerSmartContract contract = + SmartContractOuterClass.TriggerSmartContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS))) + .setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(contractAddress))) + .build(); + TransactionCapsule trxCap = wallet.createTransactionCapsule(contract, + ContractType.TriggerSmartContract); + + boolean preDebug = CommonParameter.getInstance().debug; + CommonParameter.getInstance().debug = true; + try { + Transaction tx = wallet.triggerConstantContract(contract, trxCap, + GrpcAPI.TransactionExtention.newBuilder(), GrpcAPI.Return.newBuilder()); + Assert.assertEquals(Transaction.Result.code.FAILED, tx.getRet(0).getRet()); + Assert.assertEquals(Transaction.Result.contractResult.REVERT, + tx.getRet(0).getContractRet()); + } finally { + CommonParameter.getInstance().debug = preDebug; + } + } + @Test public void testListNodes() { try { diff --git a/framework/src/test/java/org/tron/core/db/LowestBlockNumOfReceiptStoreInitTest.java b/framework/src/test/java/org/tron/core/db/LowestBlockNumOfReceiptStoreInitTest.java new file mode 100644 index 00000000000..2592cc417b1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/LowestBlockNumOfReceiptStoreInitTest.java @@ -0,0 +1,26 @@ +package org.tron.core.db; + +import javax.annotation.Resource; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; +import org.tron.core.store.TransactionRetStore; + +public class LowestBlockNumOfReceiptStoreInitTest extends BaseTest { + + @Resource + private TransactionRetStore transactionRetStore; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + @Test + public void contextStartupInitializesLowestBlockNumOfReceiptStore() { + Assert.assertFalse(transactionRetStore.getLowestReceiptBlockNum().isPresent()); + Assert.assertEquals(chainBaseManager.getHeadBlockNum() + 1, + chainBaseManager.getLowestBlockNumOfReceiptStore()); + } +} 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..478d5c0fbd7 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,63 @@ public void before() { transactionStore.put(transactionId, transactionCapsule); } + @Test + public void getLowestReceiptBlockNum() { + Assert.assertEquals(1L, transactionRetStore.getLowestReceiptBlockNum().getAsLong()); + } + + @Test + public void getLowestReceiptBlockNumPicksMinimumKey() { + transactionRetStore.put(ByteArray.fromLong(7), transactionRetCapsule); + transactionRetStore.put(ByteArray.fromLong(3), transactionRetCapsule); + try { + Assert.assertEquals(1L, transactionRetStore.getLowestReceiptBlockNum().getAsLong()); + transactionRetStore.delete(blockNum); + Assert.assertEquals(3L, transactionRetStore.getLowestReceiptBlockNum().getAsLong()); + } finally { + transactionRetStore.delete(ByteArray.fromLong(3)); + transactionRetStore.delete(ByteArray.fromLong(7)); + } + } + + @Test + public void getLowestReceiptBlockNumOnEmptyStore() { + transactionRetStore.delete(blockNum); + Assert.assertFalse(transactionRetStore.getLowestReceiptBlockNum().isPresent()); + } + + @Test + public void initLowestBlockNumOfReceiptStoreReadsStore() { + // the init runs after checkpoint recovery, so it must reflect whatever the store holds + // at call time: the first key while present, the next block once the store is empty + chainBaseManager.initLowestBlockNumOfReceiptStore(); + Assert.assertEquals(1L, chainBaseManager.getLowestBlockNumOfReceiptStore()); + + // head must be non-zero, otherwise head + 1 collides with the first key asserted above + transactionRetStore.delete(blockNum); + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(5); + try { + chainBaseManager.initLowestBlockNumOfReceiptStore(); + Assert.assertEquals(6L, chainBaseManager.getLowestBlockNumOfReceiptStore()); + } finally { + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderNumber(0); + } + } + + @Test + public void initLowestBlockNumOfReceiptStoreWithHistoryOff() { + String historySwitch = Args.getInstance().getStorage().getTransactionHistorySwitch(); + Args.getInstance().getStorage().setTransactionHistorySwitch("off"); + try { + chainBaseManager.initLowestBlockNumOfReceiptStore(); + Assert.assertTrue(transactionRetStore.getLowestReceiptBlockNum().isPresent()); + Assert.assertEquals(Long.MAX_VALUE, chainBaseManager.getLowestBlockNumOfReceiptStore()); + } finally { + Args.getInstance().getStorage().setTransactionHistorySwitch(historySwitch); + chainBaseManager.initLowestBlockNumOfReceiptStore(); + } + } + @Test public void get() throws BadItemException { TransactionInfoCapsule resultCapsule = transactionRetStore.getTransactionInfo(transactionId); @@ -78,7 +135,12 @@ public void put() { Assert.assertNull("put transaction info error", transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); transactionRetStore.put(transactionInfoCapsule.getId(), transactionRetCapsule); - Assert.assertNotNull("get transaction info error", - transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); + try { + Assert.assertNotNull("get transaction info error", + transactionRetStore.getUnchecked(transactionInfoCapsule.getId())); + } finally { + // a 32-byte key left behind breaks getNext's fixed-length key comparison + transactionRetStore.delete(transactionInfoCapsule.getId()); + } } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java index 6df63c6c04e..fd0c5e4b0ce 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java @@ -16,11 +16,13 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import org.tron.api.GrpcAPI; import org.tron.common.TestConstants; +import org.tron.common.VMConfigRule; import org.tron.common.application.TronApplicationContext; import org.tron.common.logsfilter.EventPluginConfig; import org.tron.common.logsfilter.EventPluginLoader; @@ -53,6 +55,9 @@ public class BlockEventGetTest extends BlockGenerate { @ClassRule public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public final VMConfigRule vmConfigRule = new VMConfigRule(); + static ChainBaseManager chainManager; private final String key = PublicMethod.getRandomPrivateKey(); 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 be3098095d5..d55e6216358 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 c35b82a4d30..e1286c6e407 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -17,12 +17,15 @@ 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.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.NodeInfoService; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.core.vm.program.Program; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; public class JsonRpcCallAndEstimateGasTest { @@ -86,76 +89,91 @@ public void testEstimateGasKeepsInvalidQuantityAndDataAsInvalidParams() throws E 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 @@ -171,6 +189,68 @@ public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exc Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); } + @Test + public void testGetCallNonRevertFailureIsNotExecutionReverted() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetCallNonRevertFailureAttachesReturnData() throws Exception { + byte[] resData = ByteArray.fromHexString("deadbeef00"); + mockRpc = newRpcWithMockedFailedCall(resData, contractResult.DEFAULT, + "Unknown failure", EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Unknown failure", e.getMessage()); + Assert.assertEquals("0xdeadbeef00", e.getData()); + } + + @Test + public void testGetCallThrownVmExceptionIsInternalError() throws Exception { + // for a plain eth_call, Wallet rethrows every non-revert VM failure instead of + // returning a FAILED result, so it must surface as -32000 without data + mockRpc = newRpcWithThrowingCall(new Program.OutOfEnergyException("Not enough energy")); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals("Not enough energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testEstimateGasWithEstimateEnergyNonRevertFailureIsNotExecutionReverted() + throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testEstimateGasWithEstimateEnergyNonRevertFailureAttachesReturnData() + throws Exception { + byte[] resData = ByteArray.fromHexString("deadbeef00"); + mockRpc = newRpcWithMockedFailedCall(resData, contractResult.OUT_OF_ENERGY, + "Out of energy", EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals("Out of energy", e.getMessage()); + Assert.assertEquals("0xdeadbeef00", e.getData()); + } + @Test public void testGetCallReturnsConstantResult() throws Exception { byte[] part1 = ByteArray.fromHexString("deadbeef"); @@ -204,8 +284,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); @@ -214,6 +294,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 -> { @@ -221,12 +307,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())) @@ -234,11 +317,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; }); } @@ -246,6 +326,22 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima return rpc; } + private static TronJsonRpcImpl newRpcWithThrowingCall(RuntimeException vmException) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenThrow(vmException); + + TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + return rpc; + } + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) throws Exception { Wallet mockWallet = mock(Wallet.class); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java new file mode 100644 index 00000000000..9d62196a9f6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java @@ -0,0 +1,429 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Strings; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.TransactionInfoList; +import org.tron.core.Wallet; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; + +public class JsonRpcPrunedHistoryTest { + + private static final long LOWEST_BLOCK_NUM = 100L; + private static final long LOWEST_RECEIPT_BLOCK_NUM = 150L; + private static final long HEAD_BLOCK_NUM = 200L; + private static final String PRUNED_MESSAGE = "Pruned history unavailable"; + private static final String BELOW_CUTOFF_HEX = "0x10"; + private static final String AT_CUTOFF_HEX = "0x64"; + private static final long IN_RECEIPT_GAP_NUM = 112L; + private static final String IN_RECEIPT_GAP_HEX = "0x70"; + private static final String LOWEST_RECEIPT_BLOCK_HEX = "0x96"; + private static final String INVALID_INDEX = "1a"; + private static final String UNKNOWN_HASH = "0x" + Strings.repeat("ab", 32); + + private TronJsonRpcImpl rpc; + + @After + public void tearDown() throws IOException { + if (rpc != null) { + rpc.close(); + rpc = null; + } + } + + private static Block newBlock(long number, int transactionCount) { + Block.Builder builder = Block.newBuilder().setBlockHeader(BlockHeader.newBuilder() + .setRawData(BlockHeader.raw.newBuilder().setNumber(number))); + for (int i = 0; i < transactionCount; i++) { + builder.addTransactions(Transaction.newBuilder()); + } + return builder.build(); + } + + private static Wallet newMockWallet(boolean liteNode) { + Wallet wallet = mock(Wallet.class); + when(wallet.isLiteNode()).thenReturn(liteNode); + when(wallet.getLowestBlockNum()).thenReturn(liteNode ? LOWEST_BLOCK_NUM : 0L); + when(wallet.getLowestBlockNumOfReceiptStore()) + .thenReturn(liteNode ? LOWEST_RECEIPT_BLOCK_NUM : 0L); + when(wallet.getCursor()).thenReturn(Chainbase.Cursor.HEAD); + when(wallet.getNowBlock()).thenReturn(newBlock(HEAD_BLOCK_NUM, 0)); + // a LiteNode snapshot copies genesis explicitly, so block 0 stays retrievable below the cutoff + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 0)); + return wallet; + } + + private TronJsonRpcImpl newRpc(boolean liteNode) { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newMockWallet(liteNode), null); + return rpc; + } + + private static Wallet newHistoryOffMockWallet() { + return newHistoryOffMockWallet(true); + } + + private static Wallet newHistoryOffMockWallet(boolean liteNode) { + Wallet wallet = newMockWallet(liteNode); + when(wallet.getLowestBlockNumOfReceiptStore()).thenReturn(Long.MAX_VALUE); + return wallet; + } + + @Test + public void testParseBlockTagEarliestOnLiteNode() throws Exception { + Assert.assertEquals(LOWEST_RECEIPT_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(true))); + } + + @Test + public void testParseBlockTagEarliestOnFullNode() throws Exception { + Assert.assertEquals(0L, JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(false))); + } + + @Test + public void testGetBlockByNumberBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockByNumberAtCutoffPasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber(AT_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockByNumberEarliestOnLiteNodePasses() throws Exception { + Assert.assertNull(newRpc(true).ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetBlockByNumberOnFullNodePasses() throws Exception { + Assert.assertNull(newRpc(false).ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } + + @Test + public void testGetBlockTransactionCountGenesisOnLiteNodePasses() throws Exception { + // genesis is retained below the cutoff, so a single-block lookup must not return 4444 + Assert.assertEquals("0x0", newRpc(true).ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetBlockTransactionCountGenesisWithTransactionsOnLiteNode() throws Exception { + // Genesis carries initial-allocation transactions in its body but never has transactionInfo + // (initGenesis writes only blockStore/blockIndexStore, no processBlock). The count endpoint + // reads the body, so it must return the real count, not 4444. + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 2)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + Assert.assertEquals("0x2", rpc.ethGetBlockTransactionCountByNumber("0x0")); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexGenesisOnLiteNodePasses() throws Exception { + // index beyond the (empty) genesis body yields null, not 4444 + Assert.assertNull(newRpc(true).getTransactionByBlockNumberAndIndex("0x0", "0x0")); + } + + @Test + public void testFutureBlockOnLiteNodeReturnsNullNotPruned() throws Exception { + // above the cutoff and simply not produced yet: null, never 4444 + Assert.assertNull(newRpc(true).ethGetBlockByNumber("0x7fffffff", false)); + } + + @Test + public void testGetBlockTransactionCountBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.ethGetBlockTransactionCountByNumber(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0")); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexInvalidIndexBelowCutoff() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, INVALID_INDEX)); + } + + @Test + public void testGetTransactionByBlockNumberAndIndexInvalidIndexMissingBlock() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockNumberAndIndex(AT_CUTOFF_HEX, INVALID_INDEX)); + } + + @Test + public void testGetTransactionByBlockHashAndIndexInvalidIndexMissingBlock() { + TronJsonRpcImpl liteRpc = newRpc(true); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> liteRpc.getTransactionByBlockHashAndIndex(UNKNOWN_HASH, INVALID_INDEX)); + } + + @Test + public void testGetBlockReceiptsBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsFutureBlockWithHistoryOffReturnsNull() throws Exception { + // a block that does not exist yet is not pruned history + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(), null); + + Assert.assertNull(rpc.getBlockReceipts("0x7fffffff")); + } + + @Test + public void testGetLogsFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testNewFilterFromBlockBelowCutoffReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.newFilter(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testNewFilterEarliestOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("earliest", "latest", null, null, null); + + String filterId = newRpc(true).newFilter(fr); + + Assert.assertNotNull(filterId); + Assert.assertTrue(filterId.startsWith("0x")); + } + + @Test + public void testLogFilterGenesisOnlyRangeOnLiteNodePasses() throws Exception { + FilterRequest fr = new FilterRequest("0x0", "0x0", null, null, null); + + LogFilterWrapper wrapper = + new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + Assert.assertEquals(0L, wrapper.getToBlock()); + } + + @Test + public void testLogFilterGenesisBlockHashOnLiteNodePasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockById(org.mockito.ArgumentMatchers.any())) + .thenReturn(newBlock(0L, 0)); + FilterRequest fr = new FilterRequest(null, null, null, null, + "0x" + Strings.repeat("00", 32)); + + LogFilterWrapper wrapper = new LogFilterWrapper(fr, HEAD_BLOCK_NUM, wallet, false); + + Assert.assertEquals(0L, wrapper.getFromBlock()); + } + + @Test + public void testLogFilterEarliestWithLowToBlockIsInvalidRange() { + FilterRequest fr = new FilterRequest("earliest", "0x5", null, null, null); + + assertThrows(JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false)); + } + + @Test + public void testGetLogsInReceiptGapReturns4444() { + TronJsonRpcImpl liteRpc = newRpc(true); + FilterRequest fr = new FilterRequest(IN_RECEIPT_GAP_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> liteRpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsInReceiptGapReturns4444() { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockInReceiptGapReturns4444() { + // the check ignores the transaction count, so an empty block is not special-cased + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsAtLowestReceiptBlockPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 0)); + when(wallet.getTransactionInfoByBlockNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(TransactionInfoList.getDefaultInstance()); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + Assert.assertNotNull(rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); + } + + @Test + public void testGetBlockTransactionCountInReceiptGapPasses() throws Exception { + Wallet wallet = newMockWallet(true); + when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM)) + .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + Assert.assertEquals("0x1", rpc.ethGetBlockTransactionCountByNumber(IN_RECEIPT_GAP_HEX)); + } + + @Test + public void testParseBlockTagEarliestWithHistoryOffFallsBackToLowestBlock() throws Exception { + Assert.assertEquals(LOWEST_BLOCK_NUM, + JsonRpcApiUtil.parseBlockTag("earliest", newHistoryOffMockWallet())); + } + + @Test + public void testGetBlockByNumberEarliestWithHistoryOffPasses() throws Exception { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(), null); + + Assert.assertNull(rpc.ethGetBlockByNumber("earliest", false)); + } + + @Test + public void testGetLogsWithHistoryOffReturns4444() { + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(), null); + FilterRequest fr = new FilterRequest(LOWEST_RECEIPT_BLOCK_HEX, "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() { + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> JsonRpcApiUtil.checkPrunedReceiptHistory(Long.MAX_VALUE, newHistoryOffMockWallet())); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsEmptyBlockWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 0)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetLogsOnFullNodeWithHistoryOffReturns4444() { + // receipt persistence is a per-node switch, so the LiteNode gate must not shield a FullNode + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false), null); + FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getLogs(fr)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockReceiptsOnFullNodeWithHistoryOffReturns4444() { + Wallet wallet = newHistoryOffMockWallet(false); + when(wallet.getBlockByNum(LOWEST_RECEIPT_BLOCK_NUM)) + .thenReturn(newBlock(LOWEST_RECEIPT_BLOCK_NUM, 1)); + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet, null); + + JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class, + () -> rpc.getBlockReceipts(LOWEST_RECEIPT_BLOCK_HEX)); + Assert.assertEquals(PRUNED_MESSAGE, e.getMessage()); + Assert.assertNull(e.getData()); + } + + @Test + public void testGetBlockByNumberOnFullNodeWithHistoryOffPasses() throws Exception { + // bodies are unaffected by the receipt switch + rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet(false), null); + + Assert.assertNull(rpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false)); + } +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 45e41ec7629..08fb93fcbeb 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; @@ -410,7 +411,7 @@ public void testGetConditions() { Assert.assertArrayEquals(conditions[2][4], getBloomIndex("0x00000000000000000000000056178a0d5f301baf6cf3e1cd53d9863437345bf9")); - } catch (JsonRpcInvalidParamsException e) { + } catch (JsonRpcException e) { Assert.fail(); } } @@ -450,7 +451,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 935f71a4a66..73009cef6bf 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -63,6 +63,7 @@ import org.tron.core.db2.ISession; import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.ItemNotFoundException; +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; @@ -1277,7 +1278,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(); } @@ -1287,7 +1288,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(); } @@ -1297,7 +1298,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(); } @@ -1307,7 +1308,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(); } @@ -1317,7 +1318,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(); } @@ -1327,7 +1328,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 = @@ -1339,10 +1340,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 { @@ -1350,7 +1351,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( @@ -1363,7 +1364,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( @@ -1376,7 +1377,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(); } @@ -1391,7 +1392,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(); } @@ -1407,7 +1408,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 { @@ -1449,13 +1450,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(); } @@ -1464,13 +1465,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(); } @@ -1478,13 +1479,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(); } @@ -1492,13 +1493,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(); } @@ -1553,7 +1554,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()); @@ -1565,7 +1566,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()); @@ -1578,7 +1579,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(); } @@ -1586,7 +1587,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 { @@ -1600,7 +1601,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 { @@ -1619,7 +1620,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()); @@ -1827,28 +1828,28 @@ public void testGetBlockReceipts() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), transactionReceipt1.getLogs()[0].getBlockTimestamp()); } - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("earliest"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("latest"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("finalized"); Assert.assertFalse(transactionReceiptList.isEmpty()); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1867,7 +1868,7 @@ public void testGetBlockReceipts() { try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); Assert.assertNull(transactionReceiptList); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } @@ -1881,7 +1882,7 @@ public void testGetBlockReceipts() { Assert.assertFalse(transactionReceiptList.isEmpty()); Assert.assertEquals(JSON.toJSONString(transactionReceiptList), JSON.toJSONString(transactionReceiptList2)); - } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { + } catch (JsonRpcException e) { throw new RuntimeException(e); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java index 77f869fd5a8..5ae31e57110 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java @@ -76,7 +76,7 @@ private Manager buildMockManager(long block1, TransactionInfoList txList1, } private LogMatch buildLogMatch(List blockNums, Manager manager) - throws JsonRpcInvalidParamsException { + throws Exception { FilterRequest fr = new FilterRequest(); // match-all filter LogFilterWrapper wrapper = new LogFilterWrapper(fr, 0L, null, false); return new LogMatch(wrapper, blockNums, manager); @@ -85,8 +85,7 @@ private LogMatch buildLogMatch(List blockNums, Manager manager) /** Under the limit: all logs returned without exception. */ @Test public void testUnderLimit_returnsAllResults() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { int logCount = MAX_RESULT / 2; // 5000, well under limit Manager manager = buildMockManager(100L, buildTxList(logCount)); LogMatch logMatch = buildLogMatch(Collections.singletonList(100L), manager); @@ -101,8 +100,7 @@ public void testUnderLimit_returnsAllResults() */ @Test public void testAtExactLimit_succeeds() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 1 log → total == MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -119,7 +117,7 @@ public void testAtExactLimit_succeeds() */ @Test public void testExceedsLimit_throws() - throws ItemNotFoundException, JsonRpcInvalidParamsException { + throws Exception { // block 1: MAX_RESULT - 1 logs, block 2: 2 logs → 9999 + 2 = 10001 > MAX_RESULT Manager manager = buildMockManager( 1L, buildTxList(MAX_RESULT - 1), @@ -132,8 +130,7 @@ public void testExceedsLimit_throws() /** A block with no matching logs is skipped without incrementing the result count. */ @Test public void testEmptyBlockSkipped() - throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException, - JsonRpcInvalidParamsException { + throws Exception { // block 1: no logs (empty txInfoList → skipped), block 2: 3 logs Manager manager = mock(Manager.class); ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java index 39bcc30e278..3b7c796fbb0 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java @@ -15,6 +15,7 @@ import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.config.args.Args; import org.tron.core.exception.EventBloomException; @@ -30,6 +31,9 @@ public class SectionBloomStoreTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + @Resource + private Wallet wallet; + private ExecutorService sectionExecutor; static { @@ -145,7 +149,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", ByteArray.toJsonHex(address1), null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -162,7 +166,7 @@ public void testWriteAndQuery() { try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", addressList, null, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -178,7 +182,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic1)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -194,7 +198,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new String[] {ByteArray.toHexString(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -212,7 +216,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {topicList}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); @@ -239,7 +243,7 @@ public void testWriteAndQuery() { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("earliest", "latest", null, new Object[] {ByteArray.toJsonHex(topic1), ByteArray.toJsonHex(topic2)}, null), - currentMaxBlockNum, null, false); + currentMaxBlockNum, wallet, false); LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum, sectionExecutor); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java index 6814e843a25..f00ab3ff4c1 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 @@ -9,16 +9,22 @@ import com.googlecode.jsonrpc4j.JsonRpcErrors; import com.googlecode.jsonrpc4j.JsonRpcMethod; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.concurrent.ExecutionException; import org.junit.Assert; import org.junit.Test; import org.tron.core.exception.TronError; 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; +import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; public class JsonRpcErrorResolverTest { @@ -29,6 +35,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 = ExecutionException.class, code = -32000, data = "{}"), @JsonRpcError(exception = JsonRpcException.class, code = -1) @@ -85,6 +92,16 @@ public void testMappedErrorsPreserveCodeAndDataPriority() 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, NO_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, NO_ARGUMENTS); @@ -95,6 +112,75 @@ public void testMappedErrorsPreserveCodeAndDataPriority() throws Exception { Assert.assertNull(error.data); } + @Test + public void testPrunedHistoryMapsTo4444OnEveryDeclaringMethod() { + Map methods = getMethodsDeclaring(JsonRpcPrunedHistoryException.class); + Assert.assertEquals(new TreeSet<>(Arrays.asList("eth_getBlockByNumber", + "eth_getBlockReceipts", "eth_getBlockTransactionCountByNumber", "eth_getLogs", + "eth_getTransactionByBlockNumberAndIndex", "eth_newFilter")), methods.keySet()); + for (Map.Entry entry : methods.entrySet()) { + JsonError error = resolver.resolveError( + new JsonRpcPrunedHistoryException(JsonRpcApiUtil.PRUNED_HISTORY_ERROR), + entry.getValue(), NO_ARGUMENTS); + + Assert.assertEquals(entry.getKey(), 4444, error.code); + Assert.assertEquals(entry.getKey(), JsonRpcApiUtil.PRUNED_HISTORY_ERROR, error.message); + Assert.assertEquals(entry.getKey(), "{}", error.data); + } + } + + @Test + public void testExecutionRevertedMapsTo3OnEveryDeclaringMethod() { + Map methods = getMethodsDeclaring(JsonRpcExecutionRevertedException.class); + Assert.assertEquals(new TreeSet<>(Arrays.asList("eth_call", "eth_estimateGas")), + methods.keySet()); + String revertData = "0x08c379a0"; + for (Map.Entry entry : methods.entrySet()) { + JsonError error = resolver.resolveError( + new JsonRpcExecutionRevertedException("REVERT opcode executed", revertData), + entry.getValue(), NO_ARGUMENTS); + + Assert.assertEquals(entry.getKey(), 3, error.code); + Assert.assertEquals(entry.getKey(), "REVERT opcode executed", error.message); + Assert.assertEquals(entry.getKey(), revertData, error.data); + } + } + + private Map getMethodsDeclaring(Class exception) { + Map methods = new TreeMap<>(); + for (Method method : TronJsonRpc.class.getMethods()) { + if (Arrays.asList(method.getExceptionTypes()).contains(exception)) { + JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); + methods.put(rpcMethod == null ? method.getName() : rpcMethod.value(), method); + } + } + return methods; + } + + @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)); + } + } + } + } + @Test public void testUnmappedExceptionsUseSanitizedInternalError() throws Exception { Method method = getClass().getMethod("unmappedMethod");