diff --git a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java
index 21f0bac8d77..93e149bc472 100644
--- a/chainbase/src/main/java/org/tron/core/ChainBaseManager.java
+++ b/chainbase/src/main/java/org/tron/core/ChainBaseManager.java
@@ -9,8 +9,10 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import org.tron.common.parameter.CommonParameter;
import org.tron.common.storage.metric.DbStatService;
import org.tron.common.utils.ForkController;
import org.tron.common.utils.Sha256Hash;
@@ -244,6 +246,11 @@ public class ChainBaseManager {
@Setter
private long lowestBlockNum = -1; // except num = 0.
+ // lowest block with receipts; above lowestBlockNum on a LiteNode
+ @Getter
+ @Setter
+ private long lowestReceiptBlockNum = -1;
+
@Getter
@Setter
private long latestSaveBlockTime;
@@ -394,6 +401,15 @@ private void init() {
this.lowestBlockNum = this.blockIndexStore.getLimitNumber(1, 1).stream()
.map(BlockId::getNum).findFirst().orElse(0L);
this.nodeType = getLowestBlockNum() > 1 ? NodeType.LITE : NodeType.FULL;
+ // Probed from the store itself, not from snapshot metadata; empty store falls back to
+ // the head. This runs before checkpoint recovery, so the very first session can be one
+ // block conservative; restarts self-correct. With receipt persistence off the store
+ // never grows, so no floor exists — receipt history is simply unavailable.
+ boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance()
+ .getStorage().getTransactionHistorySwitch());
+ this.lowestReceiptBlockNum = persistReceipts
+ ? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1)
+ : Long.MAX_VALUE;
this.latestSaveBlockTime = System.currentTimeMillis();
}
diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java
index 8ff3064b73c..2f1b1ee80b6 100644
--- a/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java
+++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionResultCapsule.java
@@ -147,6 +147,10 @@ public void setErrorCode(Result.code code) {
this.transactionResult = this.transactionResult.toBuilder().setRet(code).build();
}
+ public void setResultCode(contractResult code) {
+ this.transactionResult = this.transactionResult.toBuilder().setContractRet(code).build();
+ }
+
public long getShieldedTransactionFee() {
return transactionResult.getShieldedTransactionFee();
}
@@ -184,4 +188,4 @@ public byte[] getData() {
public Result getInstance() {
return this.transactionResult;
}
-}
\ No newline at end of file
+}
diff --git a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java
index a22e69e8692..6b08c9dd69b 100644
--- a/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java
+++ b/chainbase/src/main/java/org/tron/core/store/TransactionRetStore.java
@@ -1,7 +1,10 @@
package org.tron.core.store;
+import com.google.common.primitives.Longs;
import com.google.protobuf.ByteString;
+import java.util.Map;
import java.util.Objects;
+import java.util.OptionalLong;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +40,23 @@ public void put(byte[] key, TransactionRetCapsule item) {
}
}
+ /**
+ * Lowest block number that has receipts, or empty when the store has none. On a LiteNode
+ * this is generally above the block floor: a snapshot ships block bodies but no receipts.
+ *
+ *
Startup probe only — must run before any session is built. With in-flight snapshot
+ * layers, {@code getNext} does not merge deletions correctly.
+ */
+ public OptionalLong getLowestBlockNum() {
+ Map entries = revokingDB.getNext(ByteArray.fromLong(0), 1);
+ for (byte[] key : entries.keySet()) {
+ if (key.length == Long.BYTES) {
+ return OptionalLong.of(Longs.fromByteArray(key));
+ }
+ }
+ return OptionalLong.empty();
+ }
+
public TransactionInfoCapsule getTransactionInfo(byte[] key) throws BadItemException {
long blockNumber = transactionStore.getBlockNumber(key);
if (blockNumber == -1) {
diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java
index eeb92fdbd60..8337e545463 100644
--- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java
+++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java
@@ -490,6 +490,9 @@ public class CommonParameter {
public int jsonRpcMaxLogFilterNum = 20000;
@Getter
@Setter
+ public boolean jsonRpcStrictComplianceMode = false;
+ @Getter
+ @Setter
public int maxTransactionPendingSize;
@Getter
@Setter
diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java
index 2158f56d0ba..1f66b995590 100644
--- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java
+++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java
@@ -247,6 +247,7 @@ public static class JsonRpcConfig {
private int maxAddressSize = 1000;
private int maxLogFilterNum = 20000;
private long maxMessageSize = 4194304;
+ private boolean strictComplianceMode = false;
}
@Getter
diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java
new file mode 100644
index 00000000000..6c592ea8453
--- /dev/null
+++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcExecutionRevertedException.java
@@ -0,0 +1,12 @@
+package org.tron.core.exception.jsonrpc;
+
+public class JsonRpcExecutionRevertedException extends JsonRpcInternalException {
+
+ public JsonRpcExecutionRevertedException(String message) {
+ super(message);
+ }
+
+ public JsonRpcExecutionRevertedException(String message, Object data) {
+ super(message, data);
+ }
+}
diff --git a/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java
new file mode 100644
index 00000000000..4198118f638
--- /dev/null
+++ b/common/src/main/java/org/tron/core/exception/jsonrpc/JsonRpcPrunedHistoryException.java
@@ -0,0 +1,13 @@
+package org.tron.core.exception.jsonrpc;
+
+/**
+ * Thrown when a request targets historical state that a LiteNode has pruned.
+ * Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized
+ * by the Ethereum Execution API (EIP-4444).
+ */
+public class JsonRpcPrunedHistoryException extends JsonRpcInternalException {
+
+ public JsonRpcPrunedHistoryException(String message) {
+ super(message);
+ }
+}
diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf
index 25fc4832e55..bc93372181e 100644
--- a/common/src/main/resources/reference.conf
+++ b/common/src/main/resources/reference.conf
@@ -441,6 +441,8 @@ node {
maxLogFilterNum = 20000
# Maximum JSON-RPC request body size in bytes (default 4194304, ~4MB). Independent from rpc.maxMessageSize.
maxMessageSize = 4194304
+ # Reject requests that violate JSON-RPC 2.0.
+ strictComplianceMode = false
}
# Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive.
diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java
index ac54cb2b7ff..f1d24740269 100755
--- a/framework/src/main/java/org/tron/core/Wallet.java
+++ b/framework/src/main/java/org/tron/core/Wallet.java
@@ -201,6 +201,7 @@
import org.tron.core.store.StoreFactory;
import org.tron.core.store.VotesStore;
import org.tron.core.store.WitnessStore;
+import org.tron.core.utils.ResultCodeUtil;
import org.tron.core.utils.TransactionUtil;
import org.tron.core.vm.config.VMConfig;
import org.tron.core.vm.program.Program;
@@ -236,6 +237,7 @@
import org.tron.protos.Protocol.Transaction.Contract;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.Protocol.Transaction.Result.code;
+import org.tron.protos.Protocol.Transaction.Result.contractResult;
import org.tron.protos.Protocol.TransactionInfo;
import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract;
import org.tron.protos.contract.BalanceContract;
@@ -737,6 +739,18 @@ public long getHeadBlockNum() {
return chainBaseManager.getHeadBlockNum();
}
+ public boolean isLiteNode() {
+ return chainBaseManager.isLiteNode();
+ }
+
+ public long getLowestBlockNum() {
+ return chainBaseManager.getLowestBlockNum();
+ }
+
+ public long getLowestReceiptBlockNum() {
+ return chainBaseManager.getLowestReceiptBlockNum();
+ }
+
public BlockCapsule getBlockCapsuleByNum(long blockNum) {
try {
return chainBaseManager.getBlockByNum(blockNum);
@@ -3186,12 +3200,15 @@ public Transaction callConstantContract(TransactionCapsule trxCap,
ret.setStatus(0, code.SUCESS);
if (StringUtils.isNoneEmpty(result.getRuntimeError())) {
ret.setStatus(0, code.FAILED);
+ // same failure classification as executed transactions
+ ret.setResultCode(ResultCodeUtil.resolve(result.getException()));
retBuilder
.setMessage(ByteString.copyFromUtf8(result.getRuntimeError()))
.build();
}
if (result.isRevert()) {
ret.setStatus(0, code.FAILED);
+ ret.setResultCode(contractResult.REVERT);
retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed"))
.build();
}
diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java
index 0bca242606e..f2a0b995081 100644
--- a/framework/src/main/java/org/tron/core/config/args/Args.java
+++ b/framework/src/main/java/org/tron/core/config/args/Args.java
@@ -560,6 +560,7 @@ private static void applyNodeConfig(NodeConfig nc) {
PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize();
PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum();
PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize();
+ PARAMETER.jsonRpcStrictComplianceMode = jsonrpc.isStrictComplianceMode();
// ---- P2P sub-bean ----
PARAMETER.nodeP2pVersion = nc.getP2p().getVersion();
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java
index f4bba9fbf37..594507cd277 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java
@@ -24,6 +24,7 @@
import org.tron.common.utils.StringUtil;
import org.tron.core.Wallet;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
import org.tron.protos.Protocol.Block;
import org.tron.protos.Protocol.Transaction;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
@@ -62,6 +63,7 @@ public class JsonRpcApiUtil {
public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported";
public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported";
public static final String BLOCK_NUM_ERROR = "invalid block number";
+ public static final String PRUNED_HISTORY_ERROR = "Pruned history unavailable";
public static final String TX_INDEX_ERROR = "invalid index value";
private static final SecureRandom random = new SecureRandom();
@@ -636,7 +638,13 @@ public static long parseBlockTag(String tag, Wallet wallet)
return wallet.getHeadBlockNum();
}
if (EARLIEST_STR.equalsIgnoreCase(tag)) {
- return 0;
+ if (!wallet.isLiteNode()) {
+ return 0;
+ }
+ // "earliest" anchors to the receipt floor (first block with complete data); with
+ // receipt persistence off no such block exists — fall back to the body floor
+ long receiptFloor = wallet.getLowestReceiptBlockNum();
+ return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor;
}
if (FINALIZED_STR.equalsIgnoreCase(tag)) {
return wallet.getSolidBlockNum();
@@ -700,6 +708,42 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet)
return parseBlockNumber(blockNumOrTag);
}
+ /**
+ * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444.
+ * Raw primitive — no genesis exemption; callers own that semantics.
+ */
+ public static void checkPrunedHistory(long blockNum, Wallet wallet)
+ throws JsonRpcPrunedHistoryException {
+ if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) {
+ throw new JsonRpcPrunedHistoryException(prunedMessage(wallet.getLowestBlockNum()));
+ }
+ }
+
+ /**
+ * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read
+ * receipts or logs; their floor is the first block with receipts. Same raw-primitive
+ * contract.
+ */
+ public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet)
+ throws JsonRpcPrunedHistoryException {
+ if (!wallet.isLiteNode()) {
+ return;
+ }
+ long receiptFloor = wallet.getLowestReceiptBlockNum();
+ if (receiptFloor == Long.MAX_VALUE) {
+ throw new JsonRpcPrunedHistoryException(
+ PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node");
+ }
+ if (blockNum < receiptFloor) {
+ throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor));
+ }
+ }
+
+ private static String prunedMessage(long earliestAvailable) {
+ return PRUNED_HISTORY_ERROR + ": earliest available block is 0x"
+ + Long.toHexString(earliestAvailable);
+ }
+
/**
* Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the
* longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix.
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
index ca249da4e5d..e60f59b2ba0 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java
@@ -133,6 +133,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false);
return;
}
+ if (!isBatch && violatesStrictCompliance(rootNode)) {
+ JsonNode id = rootNode.get("id");
+ writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request",
+ id != null && (id.isTextual() || id.isNumber()) ? id : null, false);
+ return;
+ }
int batchSize = parameter.getJsonRpcMaxBatchSize();
if (isBatch && batchSize > 0 && rootNode.size() > batchSize) {
writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
@@ -193,8 +199,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes
continue;
}
- if (!subRequest.isObject()) {
- ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null);
+ if (!subRequest.isObject() || violatesStrictCompliance(subRequest)) {
+ JsonNode subId = subRequest.get("id");
+ ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request",
+ subId != null && (subId.isTextual() || subId.isNumber()) ? subId : null);
byte[] errBytes = MAPPER.writeValueAsBytes(errNode);
int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0);
if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) {
@@ -265,6 +273,36 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes
resp.getOutputStream().flush();
}
+ /**
+ * Whether a request object breaks JSON-RPC 2.0 (https://www.jsonrpc.org/specification),
+ * checked only when {@code node.jsonrpc.strictComplianceMode} is on. jsonrpc4j is tolerant
+ * by default: it accepts a missing or wrong {@code jsonrpc} field and any {@code id} type.
+ */
+ private static boolean violatesStrictCompliance(JsonNode node) {
+ if (!CommonParameter.getInstance().isJsonRpcStrictComplianceMode()) {
+ return false;
+ }
+ JsonNode version = node.get("jsonrpc");
+ if (version == null || !version.isTextual() || !"2.0".equals(version.asText())) {
+ return true;
+ }
+ // A malformed request object is -32600, not the -32601 jsonrpc4j reports for a missing
+ // or non-string method.
+ JsonNode method = node.get("method");
+ if (method == null || !method.isTextual()) {
+ return true;
+ }
+ // params may be omitted; a present one must be an Array or Object per JSON-RPC 2.0 §4.2
+ JsonNode params = node.get("params");
+ if (params != null && !params.isArray() && !params.isObject()) {
+ return true;
+ }
+ // Any non-scalar id is rejected. Spec deviation: null is a valid id per JSON-RPC 2.0,
+ // but jsonrpc4j treats it as a notification and answers nothing, so -32600 beats silence.
+ JsonNode id = node.get("id");
+ return id != null && !id.isTextual() && !id.isNumber();
+ }
+
private byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java
index 50da763b8b9..5104e213788 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java
@@ -19,10 +19,12 @@
import org.tron.core.exception.BadItemException;
import org.tron.core.exception.ItemNotFoundException;
import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException;
+import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException;
import org.tron.core.exception.jsonrpc.JsonRpcInternalException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException;
import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException;
import org.tron.core.services.jsonrpc.types.BlockResult;
import org.tron.core.services.jsonrpc.types.BuildArguments;
@@ -55,8 +57,10 @@ public interface TronJsonRpc {
@JsonRpcMethod("eth_getBlockTransactionCountByNumber")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
- String ethGetBlockTransactionCountByNumber(String bnOrId) throws JsonRpcInvalidParamsException;
+ String ethGetBlockTransactionCountByNumber(String bnOrId)
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException;
@JsonRpcMethod("eth_getBlockByHash")
@JsonRpcErrors({
@@ -68,9 +72,10 @@ BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionObjects)
@JsonRpcMethod("eth_getBlockByNumber")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
BlockResult ethGetBlockByNumber(String bnOrId, Boolean fullTransactionObjects)
- throws JsonRpcInvalidParamsException;
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException;
@JsonRpcMethod("net_version")
String getNetVersion() throws JsonRpcInternalException;
@@ -120,6 +125,7 @@ String getABIOfSmartContract(String contractAddress, String bnOrId)
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"),
})
String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException,
@@ -141,9 +147,10 @@ TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String ind
@JsonRpcMethod("eth_getTransactionByBlockNumberAndIndex")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index)
- throws JsonRpcInvalidParamsException;
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException;
@JsonRpcMethod("eth_getTransactionReceipt")
@JsonRpcErrors({
@@ -154,6 +161,7 @@ TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, Stri
@JsonRpcMethod("eth_getBlockReceipts")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}")
})
List getBlockReceipts(String blockNumOrHashOrTag)
@@ -163,6 +171,7 @@ List getBlockReceipts(String blockNumOrHashOrTag)
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"),
})
String getCall(CallArguments transactionCall, Object blockNumOrTag)
@@ -292,9 +301,10 @@ CompilationResult ethSubmitHashrate(String hashrate, String id)
@JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcExceedLimitException.class, code = -32005, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
- JsonRpcMethodNotFoundException, JsonRpcExceedLimitException;
+ JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, JsonRpcPrunedHistoryException;
@JsonRpcMethod("eth_newBlockFilter")
@JsonRpcErrors({
@@ -327,6 +337,7 @@ Object[] getFilterChanges(String filterId)
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"),
@JsonRpcError(exception = JsonRpcTooManyResultException.class, code = -32005, data = "{}"),
+ @JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
@JsonRpcError(exception = BadItemException.class, code = -32000, data = "{}"),
@JsonRpcError(exception = ExecutionException.class, code = -32000, data = "{}"),
@JsonRpcError(exception = InterruptedException.class, code = -32000, data = "{}"),
@@ -334,7 +345,8 @@ Object[] getFilterChanges(String filterId)
})
LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException,
ExecutionException, InterruptedException, BadItemException, ItemNotFoundException,
- JsonRpcMethodNotFoundException, JsonRpcTooManyResultException;
+ JsonRpcMethodNotFoundException, JsonRpcTooManyResultException,
+ JsonRpcPrunedHistoryException;
@JsonRpcMethod("eth_getFilterLogs")
@JsonRpcErrors({
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java
index 6be47886117..67b74b5ca90 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java
@@ -71,10 +71,12 @@
import org.tron.core.exception.ItemNotFoundException;
import org.tron.core.exception.VMIllegalException;
import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException;
+import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException;
import org.tron.core.exception.jsonrpc.JsonRpcInternalException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException;
import org.tron.core.exception.jsonrpc.JsonRpcMethodNotFoundException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException;
import org.tron.core.services.NodeInfoService;
import org.tron.core.services.http.JsonFormat;
@@ -101,6 +103,7 @@
import org.tron.protos.Protocol.Transaction;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.Protocol.Transaction.Result.code;
+import org.tron.protos.Protocol.Transaction.Result.contractResult;
import org.tron.protos.Protocol.TransactionInfo;
import org.tron.protos.contract.AssetIssueContractOuterClass.TransferAssetContract;
import org.tron.protos.contract.BalanceContract.TransferContract;
@@ -349,7 +352,7 @@ public String ethGetBlockTransactionCountByHash(String blockHash)
@Override
public String ethGetBlockTransactionCountByNumber(String blockNumOrTag)
- throws JsonRpcInvalidParamsException {
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
Block block = getBlockByNumOrTag(blockNumOrTag);
if (block == null) {
return null;
@@ -368,7 +371,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb
@Override
public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects)
- throws JsonRpcInvalidParamsException {
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
final Block b = getBlockByNumOrTag(blockNumOrTag);
return (b == null ? null : getBlockResult(b, fullTransactionObjects));
}
@@ -394,16 +397,26 @@ private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsEx
return wallet.getBlockById(ByteString.copyFrom(bHash));
}
- private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException {
+ private Block getBlockByNumOrTag(String blockNumOrTag)
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
+ long blockNum;
if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) {
if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) {
// Return the head block directly from blockStore, bypassing blockIndexStore
// which may not yet be written when latestBlockHeaderNumber is already updated.
return wallet.getNowBlock();
}
- return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet));
+ blockNum = JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet);
+ } else {
+ blockNum = parseBlockNumber(blockNumOrTag);
+ }
+ // Reject a pruned height before touching any store, so a LiteNode pays no lookup for
+ // history it cannot serve. Genesis is exempt: a snapshot copies block 0 explicitly, and
+ // lowestBlockNum is computed from block 1 upwards, so block 0 is always retained.
+ if (blockNum > 0) {
+ JsonRpcApiUtil.checkPrunedHistory(blockNum, wallet);
}
- return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag));
+ return wallet.getBlockByNum(blockNum);
}
private BlockResult getBlockResult(Block block, boolean fullTx) {
@@ -543,6 +556,25 @@ static String tryDecodeRevertReason(byte[] resData) {
}
}
+ /**
+ * Rejects a failed constant-call execution: throws code 3 with the revert payload in data
+ * for a contract revert, -32000 for any other execution failure.
+ */
+ private void requireExecutionSuccess(TransactionExtention.Builder trxExtBuilder,
+ Return.Builder retBuilder) throws JsonRpcInternalException {
+ Transaction.Result txResult = trxExtBuilder.getTransaction().getRet(0);
+ if (txResult.getRet().equals(code.SUCESS)) {
+ return;
+ }
+ byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray();
+ String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData);
+ if (txResult.getContractRet() == contractResult.REVERT) {
+ throw new JsonRpcExecutionRevertedException(errMsg, ByteArray.toJsonHex(resData));
+ }
+ throw new JsonRpcInternalException(errMsg,
+ resData.length > 0 ? ByteArray.toJsonHex(resData) : null);
+ }
+
/**
* @param data Hash of the method signature and encoded parameters. for example:
* getMethodSign(methodName(uint256,uint256)) || data1 || data2
@@ -577,27 +609,14 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va
trxExt = trxExtBuilder.build();
}
- String result;
- if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.SUCESS)) {
- List list = trxExt.getConstantResultList();
- byte[] listBytes = new byte[0];
- for (ByteString bs : list) {
- listBytes = ByteUtil.merge(listBytes, bs.toByteArray());
- }
- result = ByteArray.toJsonHex(listBytes);
- } else {
- byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray();
- String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData);
-
- if (resData.length > 0) {
- throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData));
- } else {
- throw new JsonRpcInternalException(errMsg);
- }
+ requireExecutionSuccess(trxExtBuilder, retBuilder);
+ List list = trxExt.getConstantResultList();
+ byte[] listBytes = new byte[0];
+ for (ByteString bs : list) {
+ listBytes = ByteUtil.merge(listBytes, bs.toByteArray());
}
-
- return result;
+ return ByteArray.toJsonHex(listBytes);
}
@Override
@@ -730,25 +749,12 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept
throw new JsonRpcInternalException(errString);
}
- if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) {
- byte[] data = trxExtBuilder.getConstantResult(0).toByteArray();
- String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data);
-
- if (data.length > 0) {
- throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data));
- } else {
- throw new JsonRpcInternalException(errMsg);
- }
-
- } else {
-
- if (supportEstimateEnergy) {
- return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired());
- } else {
- return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed());
- }
+ requireExecutionSuccess(trxExtBuilder, retBuilder);
+ if (supportEstimateEnergy) {
+ return ByteArray.toJsonHex(estimateBuilder.getEnergyRequired());
}
+ return ByteArray.toJsonHex(trxExtBuilder.getEnergyUsed());
}
@Override
@@ -844,7 +850,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str
@Override
public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index)
- throws JsonRpcInvalidParamsException {
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
Block block = getBlockByNumOrTag(blockNumOrTag);
if (block == null) {
return null;
@@ -947,13 +953,18 @@ public List getBlockReceipts(String blockNumOrHashOrTag)
BlockCapsule blockCapsule = new BlockCapsule(block);
long blockNum = blockCapsule.getNum();
+ int transactionSizeInBlock = blockCapsule.getTransactions().size();
+ // an empty block trivially has no receipts; for the rest, below the receipt floor the
+ // body exists but the receipts do not — 4444, not -32000
+ if (transactionSizeInBlock > 0) {
+ JsonRpcApiUtil.checkPrunedReceiptHistory(blockNum, wallet);
+ }
TransactionInfoList transactionInfoList = wallet.getTransactionInfoByBlockNum(blockNum);
// energy price at the block timestamp
long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp());
// Validate transaction list size consistency
- int transactionSizeInBlock = blockCapsule.getTransactions().size();
if (transactionSizeInBlock != transactionInfoList.getTransactionInfoCount()) {
throw new JsonRpcInternalException(
String.format("TransactionList size mismatch: "
@@ -1438,7 +1449,8 @@ public CompilationResult ethSubmitHashrate(String hashrate, String id)
@Override
public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
- JsonRpcMethodNotFoundException, JsonRpcExceedLimitException {
+ JsonRpcMethodNotFoundException, JsonRpcExceedLimitException,
+ JsonRpcPrunedHistoryException {
disableInPBFT("eth_newFilter");
// not supports finalized as block parameter
@@ -1537,7 +1549,8 @@ public Object[] getFilterChanges(String filterId) throws ItemNotFoundException,
@Override
public LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException,
ExecutionException, InterruptedException, BadItemException, ItemNotFoundException,
- JsonRpcMethodNotFoundException, JsonRpcTooManyResultException {
+ JsonRpcMethodNotFoundException, JsonRpcTooManyResultException,
+ JsonRpcPrunedHistoryException {
disableInPBFT("eth_getLogs");
long currentMaxBlockNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber();
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java
index 57739819d1e..6c3296e5c0c 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterAndResult.java
@@ -6,6 +6,7 @@
import lombok.Getter;
import org.tron.core.Wallet;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement;
@@ -15,7 +16,7 @@ public class LogFilterAndResult extends FilterResult {
private final LogFilterWrapper logFilterWrapper;
public LogFilterAndResult(FilterRequest fr, long currentMaxBlockNum, Wallet wallet)
- throws JsonRpcInvalidParamsException {
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
// eth_newFilter, no need to check block range
this.logFilterWrapper = new LogFilterWrapper(fr, currentMaxBlockNum, wallet, false);
result = new LinkedBlockingQueue<>();
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java
index 0fdf174bb50..4d3871d5479 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java
@@ -9,6 +9,7 @@
import org.tron.core.Wallet;
import org.tron.core.config.args.Args;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
import org.tron.core.services.jsonrpc.JsonRpcApiUtil;
import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
import org.tron.protos.Protocol.Block;
@@ -25,7 +26,8 @@ public class LogFilterWrapper {
private final long toBlock;
public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet,
- boolean checkBlockRange) throws JsonRpcInvalidParamsException {
+ boolean checkBlockRange)
+ throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException {
// 1.convert FilterRequest to LogFilter
this.logFilter = new LogFilter(fr);
@@ -103,6 +105,12 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet
this.fromBlock = fromBlockSrc;
this.toBlock = toBlockSrc;
+ // Reject a range starting below the receipt floor with 4444. Exception: a genesis-only
+ // query (from = to = 0, or the genesis blockHash) — block 0 is retained.
+ if (wallet != null && !(fromBlockSrc == 0 && toBlockSrc == 0)) {
+ JsonRpcApiUtil.checkPrunedReceiptHistory(fromBlockSrc, wallet);
+ }
+
// eth_getLogs enforces the block range at construction time. eth_newFilter creates the
// wrapper with checkBlockRange=false (no creation-time gate); eth_getFilterLogs re-runs this
// check against the current head before scanning so the cap cannot be bypassed.
diff --git a/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java
new file mode 100644
index 00000000000..ec2703331af
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/utils/ResultCodeUtil.java
@@ -0,0 +1,48 @@
+package org.tron.core.utils;
+
+import org.tron.core.vm.program.Program;
+import org.tron.protos.Protocol.Transaction.Result.contractResult;
+
+/**
+ * Maps a TVM execution exception to its {@code contractResult} code for constant-call
+ * responses. Deliberately independent of RuntimeImpl's consensus-path classification.
+ */
+public class ResultCodeUtil {
+
+ public static contractResult resolve(RuntimeException exception) {
+ if (exception instanceof Program.IllegalOperationException) {
+ return contractResult.ILLEGAL_OPERATION;
+ }
+ if (exception instanceof Program.OutOfEnergyException) {
+ return contractResult.OUT_OF_ENERGY;
+ }
+ if (exception instanceof Program.BadJumpDestinationException) {
+ return contractResult.BAD_JUMP_DESTINATION;
+ }
+ if (exception instanceof Program.OutOfTimeException) {
+ return contractResult.OUT_OF_TIME;
+ }
+ if (exception instanceof Program.OutOfMemoryException) {
+ return contractResult.OUT_OF_MEMORY;
+ }
+ if (exception instanceof Program.PrecompiledContractException) {
+ return contractResult.PRECOMPILED_CONTRACT;
+ }
+ if (exception instanceof Program.StackTooSmallException) {
+ return contractResult.STACK_TOO_SMALL;
+ }
+ if (exception instanceof Program.StackTooLargeException) {
+ return contractResult.STACK_TOO_LARGE;
+ }
+ if (exception instanceof Program.JVMStackOverFlowException) {
+ return contractResult.JVM_STACK_OVER_FLOW;
+ }
+ if (exception instanceof Program.TransferException) {
+ return contractResult.TRANSFER_FAILED;
+ }
+ if (exception instanceof Program.InvalidCodeException) {
+ return contractResult.INVALID_CODE;
+ }
+ return contractResult.UNKNOWN;
+ }
+}
diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf
index 1176dd46311..fbdafc78310 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -163,6 +163,9 @@ node {
maxResponseSize = 26214400
maxLogFilterNum = 20000
maxMessageSize = 4194304
+
+ # Reject requests that violate JSON-RPC 2.0.
+ # strictComplianceMode = false
}
disabledApi = [
diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java
index 3a13c7d5606..dfa1614c7f1 100644
--- a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java
+++ b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java
@@ -59,6 +59,31 @@ public void before() {
transactionStore.put(transactionId, transactionCapsule);
}
+ @Test
+ public void getLowestBlockNum() {
+ Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong());
+ }
+
+ @Test
+ public void getLowestBlockNumPicksMinimumKey() {
+ transactionRetStore.put(ByteArray.fromLong(7), transactionRetCapsule);
+ transactionRetStore.put(ByteArray.fromLong(3), transactionRetCapsule);
+ try {
+ Assert.assertEquals(1L, transactionRetStore.getLowestBlockNum().getAsLong());
+ transactionRetStore.delete(blockNum);
+ Assert.assertEquals(3L, transactionRetStore.getLowestBlockNum().getAsLong());
+ } finally {
+ transactionRetStore.delete(ByteArray.fromLong(3));
+ transactionRetStore.delete(ByteArray.fromLong(7));
+ }
+ }
+
+ @Test
+ public void getLowestBlockNumOnEmptyStore() {
+ transactionRetStore.delete(blockNum);
+ Assert.assertFalse(transactionRetStore.getLowestBlockNum().isPresent());
+ }
+
@Test
public void get() throws BadItemException {
TransactionInfoCapsule resultCapsule = transactionRetStore.getTransactionInfo(transactionId);
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java
index 33835c482fe..8f444806b55 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java
@@ -11,7 +11,6 @@
import org.tron.common.logsfilter.capsule.LogsFilterCapsule;
import org.tron.common.runtime.vm.DataWord;
import org.tron.common.runtime.vm.LogInfo;
-import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
import org.tron.core.services.jsonrpc.TronJsonRpcImpl;
import org.tron.core.services.jsonrpc.filters.FilterResult;
@@ -45,7 +44,7 @@ private TransactionInfo buildTxInfoWithLog(byte[] address) {
* Events dispatched to a matching filter in the serial (<=10000 entries) path.
*/
@Test
- public void testMatchingFilter_receivesLogElements() throws JsonRpcInvalidParamsException {
+ public void testMatchingFilter_receivesLogElements() throws Exception {
FilterRequest fr = new FilterRequest();
LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult);
@@ -64,7 +63,7 @@ public void testMatchingFilter_receivesLogElements() throws JsonRpcInvalidParams
* Filter with fromBlock=100 does not receive a capsule whose blockNumber is 50.
*/
@Test
- public void testBlockNumberBelowRange_noResult() throws JsonRpcInvalidParamsException {
+ public void testBlockNumberBelowRange_noResult() throws Exception {
FilterRequest fr = new FilterRequest();
// currentMaxBlockNum=100 → fromBlock=100, toBlock=MAX_VALUE
LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
@@ -110,7 +109,7 @@ public void testExpiredFilter_removedFromMap() throws Exception {
* A solidified capsule is routed only to the solidity map; the full-node map is untouched.
*/
@Test
- public void testSolidifiedCapsule_routedToSolidityMap() throws JsonRpcInvalidParamsException {
+ public void testSolidifiedCapsule_routedToSolidityMap() throws Exception {
FilterRequest fr = new FilterRequest();
LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null);
jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter);
@@ -133,7 +132,7 @@ public void testSolidifiedCapsule_routedToSolidityMap() throws JsonRpcInvalidPar
* A non-solidified capsule is routed only to the full-node map.
*/
@Test
- public void testNonSolidifiedCapsule_routedToFullMap() throws JsonRpcInvalidParamsException {
+ public void testNonSolidifiedCapsule_routedToFullMap() throws Exception {
FilterRequest fr = new FilterRequest();
LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null);
jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter);
@@ -156,7 +155,7 @@ public void testNonSolidifiedCapsule_routedToFullMap() throws JsonRpcInvalidPara
* Both filters in the map receive events when both match.
*/
@Test
- public void testMultipleMatchingFilters_bothReceiveEvents() throws JsonRpcInvalidParamsException {
+ public void testMultipleMatchingFilters_bothReceiveEvents() throws Exception {
FilterRequest fr = new FilterRequest();
LogFilterAndResult filter1 = new LogFilterAndResult(fr, 100L, null);
LogFilterAndResult filter2 = new LogFilterAndResult(fr, 100L, null);
@@ -178,7 +177,7 @@ public void testMultipleMatchingFilters_bothReceiveEvents() throws JsonRpcInvali
* An empty txInfoList produces no results.
*/
@Test
- public void testEmptyTxInfoList_noResult() throws JsonRpcInvalidParamsException {
+ public void testEmptyTxInfoList_noResult() throws Exception {
FilterRequest fr = new FilterRequest();
LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult);
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java
index 2ab455fa580..4cf6f76032e 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java
@@ -17,11 +17,13 @@
import org.tron.core.Wallet;
import org.tron.core.capsule.TransactionCapsule;
import org.tron.core.db.Manager;
+import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException;
import org.tron.core.exception.jsonrpc.JsonRpcInternalException;
import org.tron.core.services.NodeInfoService;
import org.tron.core.services.jsonrpc.TronJsonRpcImpl;
import org.tron.core.services.jsonrpc.types.CallArguments;
import org.tron.protos.Protocol;
+import org.tron.protos.Protocol.Transaction.Result.contractResult;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract;
public class JsonRpcCallAndEstimateGasTest {
@@ -55,76 +57,91 @@ public void tearDown() throws Exception {
public void testGetCallAppendsRevertReason() throws Exception {
byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX);
- mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL);
+ mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.CONSTANT_CALL);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.getCall(newCallArgs(), "latest"));
Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage());
+ Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData());
}
@Test
public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception {
- byte[] panicData = ByteArray.fromHexString("4e487b71"
- + "0000000000000000000000000000000000000000000000000000000000000001");
+ String panicHex = "4e487b71"
+ + "0000000000000000000000000000000000000000000000000000000000000001";
+ byte[] panicData = ByteArray.fromHexString(panicHex);
- mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL);
+ mockRpc = newRpcWithMockedFailedCall(panicData, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.CONSTANT_CALL);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.getCall(newCallArgs(), "latest"));
Assert.assertEquals(REVERT_MSG, e.getMessage());
+ Assert.assertEquals("0x" + panicHex, e.getData());
}
@Test
public void testGetCallSkipsRevertReasonForShortData() throws Exception {
- mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL);
+ mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.CONSTANT_CALL);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.getCall(newCallArgs(), "latest"));
Assert.assertEquals(REVERT_MSG, e.getMessage());
+ Assert.assertEquals("0x010203", e.getData());
}
@Test
public void testEstimateGasAppendsRevertReason() throws Exception {
byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX);
- mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL);
+ mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.CONSTANT_CALL);
CommonParameter.getInstance().setEstimateEnergy(false);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.estimateGas(newCallArgs()));
Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage());
+ Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData());
}
@Test
public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception {
- mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL);
+ mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.REVERT,
+ REVERT_MSG, EstimatePath.CONSTANT_CALL);
CommonParameter.getInstance().setEstimateEnergy(false);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.estimateGas(newCallArgs()));
Assert.assertEquals(REVERT_MSG, e.getMessage());
+ Assert.assertEquals("0x", e.getData());
}
@Test
public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception {
byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX);
- mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY);
+ mockRpc = newRpcWithMockedFailedCall(revertData, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.ESTIMATE_ENERGY);
CommonParameter.getInstance().setEstimateEnergy(true);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.estimateGas(newCallArgs()));
Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage());
+ Assert.assertEquals("0x" + ERROR_REVERT_HEX, e.getData());
}
@Test
public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception {
- mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY);
+ mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, contractResult.REVERT,
+ REVERT_MSG, EstimatePath.ESTIMATE_ENERGY);
CommonParameter.getInstance().setEstimateEnergy(true);
- JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ JsonRpcExecutionRevertedException e = assertThrows(JsonRpcExecutionRevertedException.class,
() -> mockRpc.estimateGas(newCallArgs()));
Assert.assertEquals(REVERT_MSG, e.getMessage());
+ Assert.assertEquals("0x010203", e.getData());
}
@Test
@@ -140,6 +157,31 @@ public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exc
Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result);
}
+ @Test
+ public void testGetCallNonRevertFailureIsNotExecutionReverted() throws Exception {
+ mockRpc = newRpcWithMockedFailedCall(new byte[0], contractResult.OUT_OF_ENERGY,
+ "Out of energy", EstimatePath.CONSTANT_CALL);
+
+ JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ () -> mockRpc.getCall(newCallArgs(), "latest"));
+ Assert.assertFalse(e instanceof JsonRpcExecutionRevertedException);
+ Assert.assertEquals("Out of energy", e.getMessage());
+ Assert.assertNull(e.getData());
+ }
+
+ @Test
+ public void testGetCallNonRevertFailureAttachesReturnData() throws Exception {
+ byte[] resData = ByteArray.fromHexString("deadbeef00");
+ mockRpc = newRpcWithMockedFailedCall(resData, contractResult.DEFAULT,
+ "Unknown failure", EstimatePath.CONSTANT_CALL);
+
+ JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class,
+ () -> mockRpc.getCall(newCallArgs(), "latest"));
+ Assert.assertFalse(e instanceof JsonRpcExecutionRevertedException);
+ Assert.assertEquals("Unknown failure", e.getMessage());
+ Assert.assertEquals("0xdeadbeef00", e.getData());
+ }
+
@Test
public void testGetCallReturnsConstantResult() throws Exception {
byte[] part1 = ByteArray.fromHexString("deadbeef");
@@ -173,8 +215,8 @@ private static CallArguments newCallArgs() {
return args;
}
- private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path)
- throws Exception {
+ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData,
+ contractResult contractRet, String message, EstimatePath path) throws Exception {
Wallet mockWallet = mock(Wallet.class);
Manager mockManager = mock(Manager.class);
NodeInfoService mockNodeInfo = mock(NodeInfoService.class);
@@ -183,6 +225,12 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima
.thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build()));
when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance());
+ Protocol.Transaction failedTransaction = Protocol.Transaction.newBuilder()
+ .addRet(Protocol.Transaction.Result.newBuilder()
+ .setRet(Protocol.Transaction.Result.code.FAILED)
+ .setContractRet(contractRet))
+ .build();
+
if (path == EstimatePath.ESTIMATE_ENERGY) {
when(mockWallet.estimateEnergy(any(), any(), any(), any(), any()))
.thenAnswer(invocation -> {
@@ -190,12 +238,9 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima
Return.Builder retBuilder = invocation.getArgument(3);
EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4);
extBuilder.addConstantResult(ByteString.copyFrom(resData));
- retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG));
+ retBuilder.setMessage(ByteString.copyFromUtf8(message));
estimateBuilder.setResult(retBuilder);
- return Protocol.Transaction.newBuilder()
- .addRet(Protocol.Transaction.Result.newBuilder()
- .setRet(Protocol.Transaction.Result.code.FAILED))
- .build();
+ return failedTransaction;
});
} else {
when(mockWallet.triggerConstantContract(any(), any(), any(), any()))
@@ -203,11 +248,8 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima
TransactionExtention.Builder extBuilder = invocation.getArgument(2);
Return.Builder retBuilder = invocation.getArgument(3);
extBuilder.addConstantResult(ByteString.copyFrom(resData));
- retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG));
- return Protocol.Transaction.newBuilder()
- .addRet(Protocol.Transaction.Result.newBuilder()
- .setRet(Protocol.Transaction.Result.code.FAILED))
- .build();
+ retBuilder.setMessage(ByteString.copyFromUtf8(message));
+ return failedTransaction;
});
}
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java
new file mode 100644
index 00000000000..1ad94adee5a
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcPrunedHistoryTest.java
@@ -0,0 +1,349 @@
+package org.tron.core.jsonrpc;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.base.Strings;
+import java.io.IOException;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.tron.api.GrpcAPI.TransactionInfoList;
+import org.tron.core.Wallet;
+import org.tron.core.db2.core.Chainbase;
+import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
+import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException;
+import org.tron.core.services.NodeInfoService;
+import org.tron.core.services.jsonrpc.JsonRpcApiUtil;
+import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
+import org.tron.core.services.jsonrpc.TronJsonRpcImpl;
+import org.tron.core.services.jsonrpc.filters.LogFilterWrapper;
+import org.tron.protos.Protocol.Block;
+import org.tron.protos.Protocol.BlockHeader;
+import org.tron.protos.Protocol.Transaction;
+
+public class JsonRpcPrunedHistoryTest {
+
+ private static final long LOWEST_BLOCK_NUM = 100L;
+ private static final long RECEIPT_FLOOR_BLOCK_NUM = 150L;
+ private static final long HEAD_BLOCK_NUM = 200L;
+ private static final String BODY_PRUNED_MESSAGE =
+ "Pruned history unavailable: earliest available block is 0x64";
+ private static final String RECEIPT_PRUNED_MESSAGE =
+ "Pruned history unavailable: earliest available block is 0x96";
+ private static final String HISTORY_OFF_PRUNED_MESSAGE =
+ "Pruned history unavailable: transaction history is not persisted on this node";
+ private static final String BELOW_CUTOFF_HEX = "0x10";
+ private static final String AT_CUTOFF_HEX = "0x64";
+ private static final long IN_RECEIPT_GAP_NUM = 112L;
+ private static final String IN_RECEIPT_GAP_HEX = "0x70";
+ private static final String RECEIPT_FLOOR_HEX = "0x96";
+
+ private TronJsonRpcImpl rpc;
+
+ @After
+ public void tearDown() throws IOException {
+ if (rpc != null) {
+ rpc.close();
+ rpc = null;
+ }
+ }
+
+ private static Block newBlock(long number, int transactionCount) {
+ Block.Builder builder = Block.newBuilder().setBlockHeader(BlockHeader.newBuilder()
+ .setRawData(BlockHeader.raw.newBuilder().setNumber(number)));
+ for (int i = 0; i < transactionCount; i++) {
+ builder.addTransactions(Transaction.newBuilder());
+ }
+ return builder.build();
+ }
+
+ private static Wallet newMockWallet(boolean liteNode) {
+ Wallet wallet = mock(Wallet.class);
+ when(wallet.isLiteNode()).thenReturn(liteNode);
+ when(wallet.getLowestBlockNum()).thenReturn(liteNode ? LOWEST_BLOCK_NUM : 0L);
+ when(wallet.getLowestReceiptBlockNum())
+ .thenReturn(liteNode ? RECEIPT_FLOOR_BLOCK_NUM : 0L);
+ when(wallet.getCursor()).thenReturn(Chainbase.Cursor.HEAD);
+ when(wallet.getNowBlock()).thenReturn(newBlock(HEAD_BLOCK_NUM, 0));
+ // a LiteNode snapshot copies genesis explicitly, so block 0 stays retrievable below the cutoff
+ when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 0));
+ return wallet;
+ }
+
+ private TronJsonRpcImpl newRpc(boolean liteNode) {
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newMockWallet(liteNode));
+ return rpc;
+ }
+
+ private static Wallet newHistoryOffMockWallet() {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getLowestReceiptBlockNum()).thenReturn(Long.MAX_VALUE);
+ return wallet;
+ }
+
+ @Test
+ public void testParseBlockTagEarliestOnLiteNode() throws Exception {
+ Assert.assertEquals(RECEIPT_FLOOR_BLOCK_NUM,
+ JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(true)));
+ }
+
+ @Test
+ public void testParseBlockTagEarliestOnFullNode() throws Exception {
+ Assert.assertEquals(0L, JsonRpcApiUtil.parseBlockTag("earliest", newMockWallet(false)));
+ }
+
+ @Test
+ public void testGetBlockByNumberBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.ethGetBlockByNumber(BELOW_CUTOFF_HEX, false));
+ Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockByNumberAtCutoffPasses() throws Exception {
+ Assert.assertNull(newRpc(true).ethGetBlockByNumber(AT_CUTOFF_HEX, false));
+ }
+
+ @Test
+ public void testGetBlockByNumberEarliestOnLiteNodePasses() throws Exception {
+ Assert.assertNull(newRpc(true).ethGetBlockByNumber("earliest", false));
+ }
+
+ @Test
+ public void testGetBlockByNumberOnFullNodePasses() throws Exception {
+ Assert.assertNull(newRpc(false).ethGetBlockByNumber(BELOW_CUTOFF_HEX, false));
+ }
+
+ @Test
+ public void testGetBlockTransactionCountGenesisOnLiteNodePasses() throws Exception {
+ // genesis is retained below the cutoff, so a single-block lookup must not return 4444
+ Assert.assertEquals("0x0", newRpc(true).ethGetBlockTransactionCountByNumber("0x0"));
+ }
+
+ @Test
+ public void testGetBlockTransactionCountGenesisWithTransactionsOnLiteNode() throws Exception {
+ // Genesis carries initial-allocation transactions in its body but never has transactionInfo
+ // (initGenesis writes only blockStore/blockIndexStore, no processBlock). The count endpoint
+ // reads the body, so it must return the real count, not 4444.
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockByNum(0L)).thenReturn(newBlock(0L, 2));
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ Assert.assertEquals("0x2", rpc.ethGetBlockTransactionCountByNumber("0x0"));
+ }
+
+ @Test
+ public void testGetTransactionByBlockNumberAndIndexGenesisOnLiteNodePasses() throws Exception {
+ // index beyond the (empty) genesis body yields null, not 4444
+ Assert.assertNull(newRpc(true).getTransactionByBlockNumberAndIndex("0x0", "0x0"));
+ }
+
+ @Test
+ public void testFutureBlockOnLiteNodeReturnsNullNotPruned() throws Exception {
+ // above the cutoff and simply not produced yet: null, never 4444
+ Assert.assertNull(newRpc(true).ethGetBlockByNumber("0x7fffffff", false));
+ }
+
+ @Test
+ public void testGetBlockTransactionCountBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.ethGetBlockTransactionCountByNumber(BELOW_CUTOFF_HEX));
+ Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetTransactionByBlockNumberAndIndexBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.getTransactionByBlockNumberAndIndex(BELOW_CUTOFF_HEX, "0x0"));
+ Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockReceiptsBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.getBlockReceipts(BELOW_CUTOFF_HEX));
+ Assert.assertEquals(BODY_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetLogsFromBlockBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+ FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.getLogs(fr));
+ Assert.assertEquals(RECEIPT_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testNewFilterFromBlockBelowCutoffReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+ FilterRequest fr = new FilterRequest("0x0", "latest", null, null, null);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.newFilter(fr));
+ Assert.assertEquals(RECEIPT_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testNewFilterEarliestOnLiteNodePasses() throws Exception {
+ FilterRequest fr = new FilterRequest("earliest", "latest", null, null, null);
+
+ String filterId = newRpc(true).newFilter(fr);
+
+ Assert.assertNotNull(filterId);
+ Assert.assertTrue(filterId.startsWith("0x"));
+ }
+
+ @Test
+ public void testLogFilterGenesisOnlyRangeOnLiteNodePasses() throws Exception {
+ FilterRequest fr = new FilterRequest("0x0", "0x0", null, null, null);
+
+ LogFilterWrapper wrapper =
+ new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false);
+
+ Assert.assertEquals(0L, wrapper.getFromBlock());
+ Assert.assertEquals(0L, wrapper.getToBlock());
+ }
+
+ @Test
+ public void testLogFilterGenesisBlockHashOnLiteNodePasses() throws Exception {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockById(org.mockito.ArgumentMatchers.any()))
+ .thenReturn(newBlock(0L, 0));
+ FilterRequest fr = new FilterRequest(null, null, null, null,
+ "0x" + Strings.repeat("00", 32));
+
+ LogFilterWrapper wrapper = new LogFilterWrapper(fr, HEAD_BLOCK_NUM, wallet, false);
+
+ Assert.assertEquals(0L, wrapper.getFromBlock());
+ }
+
+ @Test
+ public void testLogFilterEarliestWithLowToBlockIsInvalidRange() {
+ FilterRequest fr = new FilterRequest("earliest", "0x5", null, null, null);
+
+ assertThrows(JsonRpcInvalidParamsException.class,
+ () -> new LogFilterWrapper(fr, HEAD_BLOCK_NUM, newMockWallet(true), false));
+ }
+
+ @Test
+ public void testGetLogsInReceiptGapReturns4444() {
+ TronJsonRpcImpl liteRpc = newRpc(true);
+ FilterRequest fr = new FilterRequest(IN_RECEIPT_GAP_HEX, "latest", null, null, null);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> liteRpc.getLogs(fr));
+ Assert.assertEquals(RECEIPT_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockReceiptsInReceiptGapReturns4444() {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM))
+ .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1));
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX));
+ Assert.assertEquals(RECEIPT_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockReceiptsEmptyBlockInReceiptGapPasses() throws Exception {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM))
+ .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 0));
+ when(wallet.getTransactionInfoByBlockNum(IN_RECEIPT_GAP_NUM))
+ .thenReturn(TransactionInfoList.getDefaultInstance());
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ Assert.assertNotNull(rpc.getBlockReceipts(IN_RECEIPT_GAP_HEX));
+ }
+
+ @Test
+ public void testGetBlockReceiptsAtReceiptFloorPasses() throws Exception {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM))
+ .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0));
+ when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM))
+ .thenReturn(TransactionInfoList.getDefaultInstance());
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX));
+ }
+
+ @Test
+ public void testGetBlockTransactionCountInReceiptGapPasses() throws Exception {
+ Wallet wallet = newMockWallet(true);
+ when(wallet.getBlockByNum(IN_RECEIPT_GAP_NUM))
+ .thenReturn(newBlock(IN_RECEIPT_GAP_NUM, 1));
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ Assert.assertEquals("0x1", rpc.ethGetBlockTransactionCountByNumber(IN_RECEIPT_GAP_HEX));
+ }
+
+ @Test
+ public void testParseBlockTagEarliestWithHistoryOffFallsBackToBodyFloor() throws Exception {
+ Assert.assertEquals(LOWEST_BLOCK_NUM,
+ JsonRpcApiUtil.parseBlockTag("earliest", newHistoryOffMockWallet()));
+ }
+
+ @Test
+ public void testGetBlockByNumberEarliestWithHistoryOffPasses() throws Exception {
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet());
+
+ Assert.assertNull(rpc.ethGetBlockByNumber("earliest", false));
+ }
+
+ @Test
+ public void testGetLogsWithHistoryOffReturns4444() {
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), newHistoryOffMockWallet());
+ FilterRequest fr = new FilterRequest(RECEIPT_FLOOR_HEX, "latest", null, null, null);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> rpc.getLogs(fr));
+ Assert.assertEquals(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockReceiptsWithHistoryOffReturns4444() {
+ Wallet wallet = newHistoryOffMockWallet();
+ when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM))
+ .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 1));
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> rpc.getBlockReceipts(RECEIPT_FLOOR_HEX));
+ Assert.assertEquals(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testCheckPrunedReceiptHistoryAtMaxBlockWithHistoryOffThrows() {
+ JsonRpcPrunedHistoryException e = assertThrows(JsonRpcPrunedHistoryException.class,
+ () -> JsonRpcApiUtil.checkPrunedReceiptHistory(Long.MAX_VALUE, newHistoryOffMockWallet()));
+ Assert.assertEquals(HISTORY_OFF_PRUNED_MESSAGE, e.getMessage());
+ }
+
+ @Test
+ public void testGetBlockReceiptsEmptyBlockWithHistoryOffPasses() throws Exception {
+ Wallet wallet = newHistoryOffMockWallet();
+ when(wallet.getBlockByNum(RECEIPT_FLOOR_BLOCK_NUM))
+ .thenReturn(newBlock(RECEIPT_FLOOR_BLOCK_NUM, 0));
+ when(wallet.getTransactionInfoByBlockNum(RECEIPT_FLOOR_BLOCK_NUM))
+ .thenReturn(TransactionInfoList.getDefaultInstance());
+ rpc = new TronJsonRpcImpl(mock(NodeInfoService.class), wallet);
+
+ Assert.assertNotNull(rpc.getBlockReceipts(RECEIPT_FLOOR_HEX));
+ }
+}
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
index 49f875f3823..06f57750fbe 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
@@ -20,6 +20,7 @@
import org.tron.common.utils.ByteArray;
import org.tron.common.utils.ByteUtil;
import org.tron.common.utils.Commons;
+import org.tron.core.exception.jsonrpc.JsonRpcException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.services.jsonrpc.JsonRpcApiUtil;
import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
@@ -408,7 +409,7 @@ public void testGetConditions() {
Assert.assertArrayEquals(conditions[2][4],
getBloomIndex("0x00000000000000000000000056178a0d5f301baf6cf3e1cd53d9863437345bf9"));
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
}
@@ -448,7 +449,7 @@ public void testGetConditionWithHashCollision() {
Assert.assertArrayEquals(conditions[0][1],
getBloomIndex("0x3038114c1a1e72c5bfa8b003bc3650ad2ba254a0"));
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
}
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
index e8d14ace060..21d62a4f1fc 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
@@ -46,6 +46,7 @@
import org.tron.core.capsule.TransactionRetCapsule;
import org.tron.core.capsule.utils.BlockUtil;
import org.tron.core.config.args.Args;
+import org.tron.core.exception.jsonrpc.JsonRpcException;
import org.tron.core.exception.jsonrpc.JsonRpcInternalException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.services.NodeInfoService;
@@ -825,7 +826,7 @@ public void testLogFilterWrapper() {
new LogFilterWrapper(new FilterRequest(null, null, null, null, null), 100, null, false);
Assert.assertEquals(100, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -835,7 +836,7 @@ public void testLogFilterWrapper() {
new LogFilterWrapper(new FilterRequest("0x14", null, null, null, null), 100, null, false);
Assert.assertEquals(20, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -845,7 +846,7 @@ public void testLogFilterWrapper() {
new LogFilterWrapper(new FilterRequest("0x78", null, null, null, null), 100, null, false);
Assert.assertEquals(120, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -855,7 +856,7 @@ public void testLogFilterWrapper() {
new LogFilterWrapper(new FilterRequest(null, "0x14", null, null, null), 100, null, false);
Assert.assertEquals(20, logFilterWrapper.getFromBlock());
Assert.assertEquals(20, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -865,7 +866,7 @@ public void testLogFilterWrapper() {
new LogFilterWrapper(new FilterRequest(null, "0x78", null, null, null), 100, null, false);
Assert.assertEquals(100, logFilterWrapper.getFromBlock());
Assert.assertEquals(120, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -875,7 +876,7 @@ public void testLogFilterWrapper() {
null, null, null), 100, null, false);
Assert.assertEquals(20, logFilterWrapper.getFromBlock());
Assert.assertEquals(120, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
JsonRpcInvalidParamsException fromToEx =
@@ -887,10 +888,10 @@ public void testLogFilterWrapper() {
//fromBlock or toBlock is not hex num
try {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(new FilterRequest("earliest", null,
- null, null, null), 100, null, false);
+ null, null, null), 100, wallet, false);
Assert.assertEquals(0, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
@@ -898,7 +899,7 @@ public void testLogFilterWrapper() {
null, null, null), 100, null, false);
Assert.assertEquals(100, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
JsonRpcInvalidParamsException pendingFilterEx = Assert.assertThrows(
@@ -911,7 +912,7 @@ public void testLogFilterWrapper() {
null, null, null), 100, wallet, false);
Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, logFilterWrapper.getFromBlock());
Assert.assertEquals(Long.MAX_VALUE, logFilterWrapper.getToBlock());
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
JsonRpcInvalidParamsException testSyntaxEx = Assert.assertThrows(
@@ -924,7 +925,7 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -939,7 +940,7 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x0", "latest", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -955,7 +956,7 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x64", "latest", null,
null, null), 5_000, null, true);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
@@ -997,13 +998,13 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("latest", "latest", null,
null, null), LATEST_BLOCK_NUM, null, true);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
new LogFilterWrapper(new FilterRequest("latest", "latest", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -1012,13 +1013,13 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, true);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -1026,13 +1027,13 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, true);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -1040,13 +1041,13 @@ public void testLogFilterWrapper() {
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, true);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null,
null, null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -1101,7 +1102,7 @@ public void testMaxSubTopics() {
tronJsonRpc.getLogs(new FilterRequest("0xbb8", "0x1f40",
null, topics.toArray(), null));
Assert.fail("Expected to be thrown");
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.assertEquals(
"exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(),
e.getMessage());
@@ -1113,7 +1114,7 @@ public void testMaxSubTopics() {
tronJsonRpc.newFilter(new FilterRequest("0xbb8", "0x1f40",
null, topics.toArray(), null));
Assert.fail("Expected to be thrown");
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.assertEquals(
"exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(),
e.getMessage());
@@ -1126,7 +1127,7 @@ public void testMaxSubTopics() {
try {
new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40",
null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
@@ -1134,7 +1135,7 @@ public void testMaxSubTopics() {
try {
new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40",
null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
@@ -1148,7 +1149,7 @@ public void testMaxSubTopics() {
try {
new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40",
null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false);
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.fail();
}
try {
@@ -1167,7 +1168,7 @@ public void testMethodBlockRange() {
tronJsonRpc.getLogs(new FilterRequest("0x0", "0x1f40", null,
null, null));
Assert.fail("Expected to be thrown");
- } catch (JsonRpcInvalidParamsException e) {
+ } catch (JsonRpcException e) {
Assert.assertEquals(
"exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange,
e.getMessage());
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java
index 77f869fd5a8..5ae31e57110 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java
@@ -76,7 +76,7 @@ private Manager buildMockManager(long block1, TransactionInfoList txList1,
}
private LogMatch buildLogMatch(List blockNums, Manager manager)
- throws JsonRpcInvalidParamsException {
+ throws Exception {
FilterRequest fr = new FilterRequest(); // match-all filter
LogFilterWrapper wrapper = new LogFilterWrapper(fr, 0L, null, false);
return new LogMatch(wrapper, blockNums, manager);
@@ -85,8 +85,7 @@ private LogMatch buildLogMatch(List blockNums, Manager manager)
/** Under the limit: all logs returned without exception. */
@Test
public void testUnderLimit_returnsAllResults()
- throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
- JsonRpcInvalidParamsException {
+ throws Exception {
int logCount = MAX_RESULT / 2; // 5000, well under limit
Manager manager = buildMockManager(100L, buildTxList(logCount));
LogMatch logMatch = buildLogMatch(Collections.singletonList(100L), manager);
@@ -101,8 +100,7 @@ public void testUnderLimit_returnsAllResults()
*/
@Test
public void testAtExactLimit_succeeds()
- throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
- JsonRpcInvalidParamsException {
+ throws Exception {
// block 1: MAX_RESULT - 1 logs, block 2: 1 log → total == MAX_RESULT
Manager manager = buildMockManager(
1L, buildTxList(MAX_RESULT - 1),
@@ -119,7 +117,7 @@ public void testAtExactLimit_succeeds()
*/
@Test
public void testExceedsLimit_throws()
- throws ItemNotFoundException, JsonRpcInvalidParamsException {
+ throws Exception {
// block 1: MAX_RESULT - 1 logs, block 2: 2 logs → 9999 + 2 = 10001 > MAX_RESULT
Manager manager = buildMockManager(
1L, buildTxList(MAX_RESULT - 1),
@@ -132,8 +130,7 @@ public void testExceedsLimit_throws()
/** A block with no matching logs is skipped without incrementing the result count. */
@Test
public void testEmptyBlockSkipped()
- throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
- JsonRpcInvalidParamsException {
+ throws Exception {
// block 1: no logs (empty txInfoList → skipped), block 2: 3 logs
Manager manager = mock(Manager.class);
ChainBaseManager chainBaseManager = mock(ChainBaseManager.class);
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java
index 39bcc30e278..3b7c796fbb0 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java
@@ -15,6 +15,7 @@
import org.tron.common.runtime.vm.DataWord;
import org.tron.common.runtime.vm.LogInfo;
import org.tron.common.utils.ByteArray;
+import org.tron.core.Wallet;
import org.tron.core.capsule.TransactionRetCapsule;
import org.tron.core.config.args.Args;
import org.tron.core.exception.EventBloomException;
@@ -30,6 +31,9 @@ public class SectionBloomStoreTest extends BaseTest {
@Resource
SectionBloomStore sectionBloomStore;
+ @Resource
+ private Wallet wallet;
+
private ExecutorService sectionExecutor;
static {
@@ -145,7 +149,7 @@ public void testWriteAndQuery() {
try {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", ByteArray.toJsonHex(address1), null, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
@@ -162,7 +166,7 @@ public void testWriteAndQuery() {
try {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", addressList, null, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
@@ -178,7 +182,7 @@ public void testWriteAndQuery() {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", null,
new String[] {ByteArray.toHexString(topic1)}, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
@@ -194,7 +198,7 @@ public void testWriteAndQuery() {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", null,
new String[] {ByteArray.toHexString(topic2)}, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
@@ -212,7 +216,7 @@ public void testWriteAndQuery() {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", null,
new Object[] {topicList}, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
@@ -239,7 +243,7 @@ public void testWriteAndQuery() {
LogFilterWrapper logFilterWrapper = new LogFilterWrapper(
new FilterRequest("earliest", "latest", null,
new Object[] {ByteArray.toJsonHex(topic1), ByteArray.toJsonHex(topic2)}, null),
- currentMaxBlockNum, null, false);
+ currentMaxBlockNum, wallet, false);
LogBlockQuery logBlockQuery =
new LogBlockQuery(logFilterWrapper, sectionBloomStore, currentMaxBlockNum,
sectionExecutor);
diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java
index d8e64308ab8..5fe007e27dc 100644
--- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcErrorResolverTest.java
@@ -11,6 +11,7 @@
import org.junit.Assert;
import org.junit.Test;
import org.tron.core.exception.jsonrpc.JsonRpcException;
+import org.tron.core.exception.jsonrpc.JsonRpcExecutionRevertedException;
import org.tron.core.exception.jsonrpc.JsonRpcInternalException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException;
@@ -22,6 +23,7 @@ public class JsonRpcErrorResolverTest {
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"),
@JsonRpcError(exception = JsonRpcException.class, code = -1)
})
@@ -61,6 +63,16 @@ public void testResolveErrorWithTronException() throws Exception {
Assert.assertEquals(message, error.message);
Assert.assertEquals("{}", error.data);
+ message = "execution reverted";
+ data = "0x";
+ exception = new JsonRpcExecutionRevertedException(message, data);
+ error = resolver.resolveError(exception, method, arguments);
+
+ Assert.assertNotNull(error);
+ Assert.assertEquals(3, error.code);
+ Assert.assertEquals(message, error.message);
+ Assert.assertEquals(data, error.data);
+
message = "JsonRpcException";
exception = new JsonRpcException(message, null);
error = resolver.resolveError(exception, method, arguments);
@@ -72,4 +84,28 @@ public void testResolveErrorWithTronException() throws Exception {
}
-}
\ No newline at end of file
+ @Test
+ public void testAnnotationOrderNeverShadowsSubclassCodes() {
+ for (Method method : TronJsonRpc.class.getMethods()) {
+ JsonRpcErrors errors = method.getAnnotation(JsonRpcErrors.class);
+ if (errors == null) {
+ continue;
+ }
+ JsonRpcError[] entries = errors.value();
+ for (int earlier = 0; earlier < entries.length; earlier++) {
+ for (int later = earlier + 1; later < entries.length; later++) {
+ Class extends Throwable> earlierEx = entries[earlier].exception();
+ Class extends Throwable> laterEx = entries[later].exception();
+ Assert.assertFalse(String.format(
+ "%s.%s: @JsonRpcError for %s (code %d) is unreachable — its superclass %s "
+ + "(code %d) is declared before it; move the subclass entry up",
+ TronJsonRpc.class.getSimpleName(), method.getName(),
+ laterEx.getSimpleName(), entries[later].code(),
+ earlierEx.getSimpleName(), entries[earlier].code()),
+ earlierEx != laterEx && earlierEx.isAssignableFrom(laterEx));
+ }
+ }
+ }
+ }
+
+}
diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
index d6c843b5aea..ae920c5f2d1 100644
--- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -36,6 +36,7 @@ public class JsonRpcServletTest {
private JsonRpcServer mockRpcServer;
private int savedMaxBatchSize;
private int savedMaxResponseSize;
+ private boolean savedStrictComplianceMode;
@Before
public void setUp() throws Exception {
@@ -46,12 +47,14 @@ public void setUp() throws Exception {
f.set(servlet, mockRpcServer);
savedMaxBatchSize = CommonParameter.getInstance().jsonRpcMaxBatchSize;
savedMaxResponseSize = CommonParameter.getInstance().jsonRpcMaxResponseSize;
+ savedStrictComplianceMode = CommonParameter.getInstance().jsonRpcStrictComplianceMode;
}
@After
public void tearDown() {
CommonParameter.getInstance().jsonRpcMaxBatchSize = savedMaxBatchSize;
CommonParameter.getInstance().jsonRpcMaxResponseSize = savedMaxResponseSize;
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = savedStrictComplianceMode;
}
// --- parse error paths ---
@@ -355,12 +358,12 @@ public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws E
@Test
public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception {
- MockHttpServletResponse resp = doPost("[42, \"foo\", true]");
+ MockHttpServletResponse resp = doPost("[42, \"foo\", true, null]");
assertEquals(200, resp.getStatus());
JsonNode body = MAPPER.readTree(resp.getContentAsString());
assertTrue("response must be a JSON array", body.isArray());
- assertEquals(3, body.size());
- for (int i = 0; i < 3; i++) {
+ assertEquals(4, body.size());
+ for (int i = 0; i < body.size(); i++) {
assertEquals(-32600, body.get(i).get("error").get("code").asInt());
}
}
@@ -417,6 +420,181 @@ public void tooManyTokens_returnsParseError() throws Exception {
// --- helpers ---
+ // --- strict compliance mode ---
+
+ @Test
+ public void strictModeOff_tolerantOfMissingVersionAndOddId() throws Exception {
+ stubSingleOkResponse();
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_chainId\",\"id\":[1,2]}");
+
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals("default behaviour must stay tolerant", "ok", body.get("result").asText());
+ }
+
+ @Test
+ public void strictModeOn_missingVersionIsInvalidRequest() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_chainId\",\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertEquals("a scalar id is echoed back", 1, body.get("id").asInt());
+ }
+
+ @Test
+ public void strictModeOn_wrongVersionIsInvalidRequest() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"1.0\",\"method\":\"eth_chainId\",\"id\":\"a\"}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertEquals("a", body.get("id").asText());
+ }
+
+ @Test
+ public void strictModeOn_structuredIdIsInvalidRequestWithNullId() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":{\"a\":1}}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertTrue("spec §5: an undeterminable id must be null", body.get("id").isNull());
+ }
+
+ @Test
+ public void strictModeOn_missingMethodIsInvalidRequest() throws Exception {
+ // jsonrpc4j reports -32601 for these, but a malformed request object is -32600
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertEquals(1, body.get("id").asInt());
+ }
+
+ @Test
+ public void strictModeOn_nonStringMethodIsInvalidRequest() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost("{\"jsonrpc\":\"2.0\",\"method\":123,\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void strictModeOn_nonStructuredParamsIsInvalidRequest() throws Exception {
+ // without the check jsonrpc4j answers nothing at all and the caller hangs
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"params\":\"nope\",\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void strictModeOn_nullParamsIsInvalidRequest() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"params\":null,\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void strictModeOn_omittedParamsIsAccepted() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+ stubSingleOkResponse();
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals("params may be omitted", "ok", body.get("result").asText());
+ }
+
+ @Test
+ public void strictModeOn_compliantRequestStillRuns() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+ stubSingleOkResponse();
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":1}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals("ok", body.get("result").asText());
+ }
+
+ @Test
+ public void strictModeOn_explicitNullIdIsRejectedInsteadOfHanging() throws Exception {
+ // jsonrpc4j 1.6 turns an explicit null id into a notification (parseId -> null ->
+ // isNotificationRequest) and writes no response at all, so the client would wait forever.
+ // Strict mode answers -32600 instead.
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":null}");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertTrue(body.get("id").isNull());
+ }
+
+ @Test
+ public void strictModeOff_explicitNullIdKeepsLegacyNotificationBehaviour() throws Exception {
+ // Unchanged when strict mode is off: the request reaches jsonrpc4j as before.
+ MockHttpServletResponse resp = doPost(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":null}");
+
+ assertEquals(200, resp.getStatus());
+ assertEquals("no servlet-level rejection when strict mode is off",
+ 0, resp.getContentAsByteArray().length);
+ }
+
+ @Test
+ public void strictModeOn_batchValidatesEachMemberSeparately() throws Exception {
+ CommonParameter.getInstance().jsonRpcStrictComplianceMode = true;
+ byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":2}"
+ .getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ OutputStream out = inv.getArgument(1);
+ out.write(singleResp);
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
+
+ MockHttpServletResponse resp = doPost("[{\"method\":\"eth_chainId\",\"id\":1},"
+ + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\",\"id\":2}]");
+
+ JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
+ assertTrue(body.isArray());
+ assertEquals(2, body.size());
+ assertEquals("non-compliant member is rejected on its own",
+ -32600, body.get(0).get("error").get("code").asInt());
+ assertEquals("compliant member still executes", "ok", body.get(1).get("result").asText());
+ }
+
+ private void stubSingleOkResponse() throws Exception {
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ byte[] out = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
+ .getBytes(StandardCharsets.UTF_8);
+ r.getOutputStream().write(out);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ }
+
private MockHttpServletResponse doPost(String body) throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest("POST", "/jsonrpc");
req.setContent(body.getBytes(StandardCharsets.UTF_8));
diff --git a/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java
new file mode 100644
index 00000000000..266562c5eea
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/utils/ResultCodeUtilTest.java
@@ -0,0 +1,66 @@
+package org.tron.core.utils;
+
+import java.lang.reflect.Method;
+import org.junit.Assert;
+import org.junit.Test;
+import org.tron.common.runtime.ProgramResult;
+import org.tron.common.runtime.RuntimeImpl;
+import org.tron.core.vm.program.Program;
+import org.tron.core.vm.program.Program.BadJumpDestinationException;
+import org.tron.core.vm.program.Program.OutOfEnergyException;
+import org.tron.core.vm.program.Program.OutOfTimeException;
+import org.tron.protos.Protocol.Transaction.Result.contractResult;
+
+public class ResultCodeUtilTest {
+
+ @Test
+ public void testTypedExceptionsResolveToTheirCodes() {
+ Assert.assertEquals(contractResult.OUT_OF_ENERGY,
+ ResultCodeUtil.resolve(new OutOfEnergyException("out of energy")));
+ Assert.assertEquals(contractResult.OUT_OF_TIME,
+ ResultCodeUtil.resolve(new OutOfTimeException("out of time")));
+ Assert.assertEquals(contractResult.BAD_JUMP_DESTINATION,
+ ResultCodeUtil.resolve(new BadJumpDestinationException("bad jump")));
+ }
+
+ @Test
+ public void testUntypedExceptionResolvesToUnknown() {
+ Assert.assertEquals(contractResult.UNKNOWN,
+ ResultCodeUtil.resolve(new RuntimeException("untyped failure")));
+ }
+
+ @Test
+ public void testNullExceptionResolvesToUnknown() {
+ Assert.assertEquals(contractResult.UNKNOWN, ResultCodeUtil.resolve(null));
+ }
+
+ @Test
+ public void testStaysInSyncWithConsensusClassifier() throws Exception {
+ RuntimeException[] samples = {
+ new Program.IllegalOperationException("op"),
+ new Program.OutOfEnergyException("energy"),
+ new Program.BadJumpDestinationException("jump"),
+ new Program.OutOfTimeException("time"),
+ new Program.OutOfMemoryException("mem"),
+ new Program.PrecompiledContractException("pre"),
+ new Program.StackTooSmallException("small"),
+ new Program.JVMStackOverFlowException(),
+ new Program.TransferException("transfer"),
+ new Program.InvalidCodeException("code"),
+ new Program.StaticCallModificationException(),
+ new RuntimeException("untyped"),
+ };
+
+ Method consensus = RuntimeImpl.class.getDeclaredMethod("setResultCode", ProgramResult.class);
+ consensus.setAccessible(true);
+ RuntimeImpl runtime = new RuntimeImpl();
+
+ for (RuntimeException e : samples) {
+ ProgramResult result = new ProgramResult();
+ result.setException(e);
+ consensus.invoke(runtime, result);
+ Assert.assertEquals("classifiers diverged for " + e.getClass().getSimpleName(),
+ result.getResultCode(), ResultCodeUtil.resolve(e));
+ }
+ }
+}