Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions chainbase/src/main/java/org/tron/core/ChainBaseManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -184,4 +188,4 @@ public byte[] getData() {
public Result getInstance() {
return this.transactionResult;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
*
* <p>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<byte[], byte[]> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ public class CommonParameter {
public int jsonRpcMaxLogFilterNum = 20000;
@Getter
@Setter
public boolean jsonRpcStrictComplianceMode = false;
@Getter
@Setter
public int maxTransactionPendingSize;
@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions common/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions framework/src/main/java/org/tron/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Comment on lines +644 to +647

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve "earliest" from the block floor.

For a LiteNode, this code returns getLowestReceiptBlockNum(). The PR contract requires "earliest" to resolve to getLowestBlockNum(). When receipt retention starts later than block retention, block-number methods skip retained blocks.

Proposed fix
-      // "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;
+      return wallet.getLowestBlockNum();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// "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;
return wallet.getLowestBlockNum();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java`
around lines 644 - 647, Update the “earliest” resolution logic to always return
wallet.getLowestBlockNum(), removing the receiptFloor lookup and fallback so
LiteNode block-number methods use the retained block floor.

}
if (FINALIZED_STR.equalsIgnoreCase(tag)) {
return wallet.getSolidBlockNum();
Expand Down Expand Up @@ -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));
}
Comment on lines +732 to +739

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle disabled receipt persistence before comparing the floor.

When receipt persistence is disabled, receiptFloor is Long.MAX_VALUE. A request for 0x7fffffffffffffff passes the < receiptFloor check and does not return error 4444. Reject the sentinel state before the height comparison.

Proposed fix
   long receiptFloor = wallet.getLowestReceiptBlockNum();
-  if (wallet.isLiteNode() && blockNum < receiptFloor) {
-    throw new JsonRpcPrunedHistoryException(receiptFloor == Long.MAX_VALUE
-        ? PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node"
-        : prunedMessage(receiptFloor));
+  if (wallet.isLiteNode() && receiptFloor == Long.MAX_VALUE) {
+    throw new JsonRpcPrunedHistoryException(
+        PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node");
+  }
+  if (wallet.isLiteNode() && blockNum < receiptFloor) {
+    throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
long receiptFloor = wallet.getLowestReceiptBlockNum();
if (wallet.isLiteNode() && blockNum < receiptFloor) {
throw new JsonRpcPrunedHistoryException(receiptFloor == Long.MAX_VALUE
? PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node"
: prunedMessage(receiptFloor));
}
long receiptFloor = wallet.getLowestReceiptBlockNum();
if (wallet.isLiteNode() && receiptFloor == Long.MAX_VALUE) {
throw new JsonRpcPrunedHistoryException(
PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node");
}
if (wallet.isLiteNode() && blockNum < receiptFloor) {
throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java`
around lines 729 - 734, Update the receipt-history validation around
wallet.getLowestReceiptBlockNum() so a Long.MAX_VALUE receiptFloor immediately
throws JsonRpcPrunedHistoryException with the non-persisted-history message,
including when blockNum equals the sentinel. Keep the existing floor comparison
and prunedMessage(receiptFloor) behavior for finite receipt floors.

}

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Strict compliance mode rejects an explicit JSON null id with -32600, but JSON-RPC 2.0 §4.3 explicitly permits an id of String, Number, or NULL. Because a null id arrives as a Jackson NullNode, the final id != null && !id.isTextual() && !id.isNumber() check flags it as non-compliant even though it is spec-valid. This is inconsistent with the feature's stated 'align with the JSON-RPC 2.0 spec' goal and would reject spec-conforming clients that send "id":null. The anti-hang motivation for structured ids (array/object) is reasonable; consider distinguishing null ids (spec-valid, deterministic) from structured ids (undeterminable) rather than treating both identically, e.g. by rejecting only array/object ids, or by ensuring a -32600/null-id reply is written for null ids without claiming they are malformed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java, line 305:

<comment>Strict compliance mode rejects an explicit JSON `null` `id` with -32600, but JSON-RPC 2.0 §4.3 explicitly permits an id of String, Number, **or NULL**. Because a null id arrives as a Jackson NullNode, the final `id != null && !id.isTextual() && !id.isNumber()` check flags it as non-compliant even though it is spec-valid. This is inconsistent with the feature's stated 'align with the JSON-RPC 2.0 spec' goal and would reject spec-conforming clients that send `"id":null`. The anti-hang motivation for structured ids (array/object) is reasonable; consider distinguishing null ids (spec-valid, deterministic) from structured ids (undeterminable) rather than treating both identically, e.g. by rejecting only array/object ids, or by ensuring a -32600/null-id reply is written for null ids without claiming they are malformed.</comment>

<file context>
@@ -265,6 +273,38 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes
+    // becomes a notification, an array or an object makes its parseId throw — which leaves
+    // the client waiting forever, so a -32600 beats a silent hang.
+    JsonNode id = node.get("id");
+    return id != null && !id.isTextual() && !id.isNumber();
+  }
+
</file context>

Comment on lines +300 to +303

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Accept an explicit null JSON-RPC ID.

id: null is a valid JSON-RPC 2.0 request ID. Strict mode currently returns -32600 for this request.

Preserve the explicit-null request through jsonrpc4j without converting it to a notification. Return a normal response with "id": null. Add single-request and batch tests for this case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java`
around lines 300 - 303, Update the JSON-RPC request validation around the id
handling so an explicitly present null id is accepted alongside textual and
numeric ids, while a missing id remains a notification only when appropriate.
Preserve the null id through jsonrpc4j so it produces a normal response
containing "id": null, and add single-request and batch coverage for
explicit-null ids.

}

private byte[] readBody(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
Expand Down
Loading
Loading