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
22 changes: 22 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 @@ -397,6 +404,21 @@ private void init() {
this.latestSaveBlockTime = System.currentTimeMillis();
}

/**
* Probes the receipt floor from the store itself, not from snapshot metadata; an empty
* store means receipts begin with the next executed block. With receipt persistence off
* the store never grows, so no floor exists. Must run after checkpoint recovery (so the
* last session's tail is visible) and before any session is built ({@code getNext} does
* not merge in-flight layers).
*/
public void probeLowestReceiptBlockNum() {
boolean persistReceipts = BooleanUtils.toBoolean(CommonParameter.getInstance()
.getStorage().getTransactionHistorySwitch());
this.lowestReceiptBlockNum = persistReceipts
? this.transactionRetStore.getLowestBlockNum().orElseGet(() -> getHeadBlockNum() + 1)
: Long.MAX_VALUE;
}

public void shutdown() {
dbStatService.shutdown();
}
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
@@ -0,0 +1,12 @@
package org.tron.core.exception.jsonrpc;

public class JsonRpcExecutionRevertedException extends JsonRpcException {

public JsonRpcExecutionRevertedException(String message) {
super(message);
}

public JsonRpcExecutionRevertedException(String message, Object data) {
super(message, data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.tron.core.exception.jsonrpc;

/**
* Thrown when a request targets historical state that a LiteNode has pruned.
* Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized
* by the Ethereum Execution API (EIP-4444).
*/
public class JsonRpcPrunedHistoryException extends JsonRpcException {

public JsonRpcPrunedHistoryException(String message) {
super(message);
}

public JsonRpcPrunedHistoryException(String message, Object data) {
super(message, data);
}
}
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
1 change: 1 addition & 0 deletions framework/src/main/java/org/tron/core/db/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ public void init() {
trieService.setChainBaseManager(chainBaseManager);
revokingStore.disable();
revokingStore.check();
chainBaseManager.probeLowestReceiptBlockNum();
transactionCache.initCache();
rewardViCalService.init();
this.setProposalController(ProposalController.createInstance(this));
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,14 @@ public static long parseBlockTag(String tag, Wallet wallet)
return wallet.getHeadBlockNum();
}
if (EARLIEST_STR.equalsIgnoreCase(tag)) {
return 0;
if (!wallet.isLiteNode()) {
return 0;
}
// "earliest" is the lowest block for which everything the node persists is available:
// the receipt floor when receipts are persisted, otherwise the body floor (receipt
// endpoints answer 4444 on such a node regardless of this value)
long receiptFloor = wallet.getLowestReceiptBlockNum();
return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor;
}
if (FINALIZED_STR.equalsIgnoreCase(tag)) {
return wallet.getSolidBlockNum();
Expand Down Expand Up @@ -700,6 +709,43 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet)
return parseBlockNumber(blockNumOrTag);
}

/**
* Rejects a query for a block below the LiteNode pruning cutoff with error code 4444.
* Raw primitive — no genesis exemption; callers own that semantics.
*/
public static void checkPrunedHistory(long blockNum, Wallet wallet)
throws JsonRpcPrunedHistoryException {
if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) {
throw prunedHistory(wallet.getLowestBlockNum());
}
}

/**
* Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read
* receipts or logs; their floor is the first block with receipts. Same raw-primitive
* contract. Receipt persistence is a per-node switch independent of node type, so a node
* that never persists receipts is rejected before the LiteNode gate.
*/
public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet)
throws JsonRpcPrunedHistoryException {
long receiptFloor = wallet.getLowestReceiptBlockNum();
if (receiptFloor == Long.MAX_VALUE) {
throw new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR);
}
if (wallet.isLiteNode() && blockNum < receiptFloor) {
throw prunedHistory(receiptFloor);
}
Comment on lines +731 to +737

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.

}

/**
* The Execution API fixes the message verbatim; the earliest available block travels in
* {@code data} so a client can pick a fallback node from it.
*/
private static JsonRpcPrunedHistoryException prunedHistory(long earliestAvailable) {
return new JsonRpcPrunedHistoryException(PRUNED_HISTORY_ERROR,
"0x" + Long.toHexString(earliestAvailable));
}

/**
* Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the
* longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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({
Expand All @@ -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;
Expand Down Expand Up @@ -120,10 +125,12 @@ String getABIOfSmartContract(String contractAddress, String bnOrId)
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"),
})
String estimateGas(CallArguments args) throws JsonRpcInvalidRequestException,
JsonRpcInvalidParamsException, JsonRpcInternalException;
JsonRpcInvalidParamsException, JsonRpcInternalException,
JsonRpcExecutionRevertedException;

@JsonRpcMethod("eth_getTransactionByHash")
@JsonRpcErrors({
Expand All @@ -141,9 +148,10 @@ TransactionResult getTransactionByBlockHashAndIndex(String blockHash, String ind
@JsonRpcMethod("eth_getTransactionByBlockNumberAndIndex")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index)
throws JsonRpcInvalidParamsException;
throws JsonRpcInvalidParamsException, JsonRpcPrunedHistoryException;

@JsonRpcMethod("eth_getTransactionReceipt")
@JsonRpcErrors({
Expand All @@ -154,20 +162,23 @@ TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, Stri
@JsonRpcMethod("eth_getBlockReceipts")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}")
})
List<TransactionReceipt> getBlockReceipts(String blockNumOrHashOrTag)
throws JsonRpcInvalidParamsException, JsonRpcInternalException;
throws JsonRpcInvalidParamsException, JsonRpcInternalException,
JsonRpcPrunedHistoryException;

@JsonRpcMethod("eth_call")
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcInvalidRequestException.class, code = -32600, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcExecutionRevertedException.class, code = 3, data = "{}"),
@JsonRpcError(exception = JsonRpcInternalException.class, code = -32000, data = "{}"),
})
String getCall(CallArguments transactionCall, Object blockNumOrTag)
throws JsonRpcInvalidParamsException, JsonRpcInvalidRequestException,
JsonRpcInternalException;
JsonRpcInternalException, JsonRpcExecutionRevertedException;

@JsonRpcMethod("net_peerCount")
String getPeerCount();
Expand Down Expand Up @@ -292,9 +303,10 @@ CompilationResult ethSubmitHashrate(String hashrate, String id)
@JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
@JsonRpcError(exception = JsonRpcExceedLimitException.class, code = -32005, data = "{}"),
@JsonRpcError(exception = JsonRpcPrunedHistoryException.class, code = 4444, data = "{}"),
})
String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
JsonRpcMethodNotFoundException, JsonRpcExceedLimitException;
JsonRpcMethodNotFoundException, JsonRpcExceedLimitException, JsonRpcPrunedHistoryException;

@JsonRpcMethod("eth_newBlockFilter")
@JsonRpcErrors({
Expand Down Expand Up @@ -327,14 +339,16 @@ 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 = "{}"),
@JsonRpcError(exception = ItemNotFoundException.class, code = -32000, data = "{}"),
})
LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsException,
ExecutionException, InterruptedException, BadItemException, ItemNotFoundException,
JsonRpcMethodNotFoundException, JsonRpcTooManyResultException;
JsonRpcMethodNotFoundException, JsonRpcTooManyResultException,
JsonRpcPrunedHistoryException;

@JsonRpcMethod("eth_getFilterLogs")
@JsonRpcErrors({
Expand Down
Loading
Loading