diff --git a/common/src/main/java/org/tron/common/es/ExecutorServiceManager.java b/common/src/main/java/org/tron/common/es/ExecutorServiceManager.java
index 779a8edf75d..fb845ec211c 100644
--- a/common/src/main/java/org/tron/common/es/ExecutorServiceManager.java
+++ b/common/src/main/java/org/tron/common/es/ExecutorServiceManager.java
@@ -4,11 +4,14 @@
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.tron.common.exit.ExitManager;
@@ -35,6 +38,22 @@ public static ScheduledExecutorService newSingleThreadScheduledExecutor(String n
new ThreadFactoryBuilder().setNameFormat(name).setDaemon(isDaemon).build());
}
+ public static ForkJoinPool newForkJoinPool(String name, int parallelism) {
+ return newForkJoinPool(name, parallelism, false);
+ }
+
+ public static ForkJoinPool newForkJoinPool(String name, int parallelism, boolean isDaemon) {
+ AtomicInteger counter = new AtomicInteger(0);
+ ForkJoinPool.ForkJoinWorkerThreadFactory factory = pool -> {
+ ForkJoinWorkerThread thread =
+ ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
+ thread.setName(name + "-" + counter.getAndIncrement());
+ thread.setDaemon(isDaemon);
+ return thread;
+ };
+ return new ForkJoinPool(parallelism, factory, null, false);
+ }
+
public static ExecutorService newFixedThreadPool(String name, int fixThreads) {
return newFixedThreadPool(name, fixThreads, false);
}
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 eaf56bf48e8..f2831b4168f 100644
--- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java
+++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java
@@ -492,7 +492,18 @@ public class CommonParameter {
@Getter
@Setter
public int jsonRpcMaxBlockFilterNum = 50000;
-
+ @Getter
+ @Setter
+ public int jsonRpcMaxBatchSize = 100;
+ @Getter
+ @Setter
+ public int jsonRpcMaxResponseSize = 25 * 1024 * 1024;
+ @Getter
+ @Setter
+ public int jsonRpcMaxAddressSize = 1000;
+ @Getter
+ @Setter
+ public int jsonRpcMaxLogFilterNum = 20000;
@Getter
@Setter
public int maxTransactionPendingSize;
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 653fac2eb2c..aac3e930931 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
@@ -311,6 +311,10 @@ public void setHttpPBFTPort(int v) {
private int maxBlockRange = 5000;
private int maxSubTopics = 1000;
private int maxBlockFilterNum = 50000;
+ private int maxBatchSize = 100;
+ private int maxResponseSize = 25 * 1024 * 1024;
+ private int maxAddressSize = 1000;
+ private int maxLogFilterNum = 20000;
private long maxMessageSize = 4194304;
}
diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf
index 7f88719d7f8..76225aa0bed 100644
--- a/common/src/main/resources/reference.conf
+++ b/common/src/main/resources/reference.conf
@@ -380,15 +380,20 @@ node {
httpPBFTEnable = false
httpPBFTPort = 8565
- # Maximum blocks range for eth_getLogs, >0 otherwise no limit
+ # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit
maxBlockRange = 5000
-
- # Maximum topics within a topic criteria, >0 otherwise no limit
+ # Allowed max address count in filter request, default: 1000, <=0 means no limit
+ maxAddressSize = 1000
+ # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit
maxSubTopics = 1000
-
- # Maximum number for blockFilter
+ # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit
maxBlockFilterNum = 50000
-
+ # Allowed batch size, default: 100, <=0 means no limit
+ maxBatchSize = 100
+ # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit
+ maxResponseSize = 26214400
+ # Allowed maximum number for newFilter, <=0 means no limit
+ maxLogFilterNum = 20000
# Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize.
maxMessageSize = 4M
}
diff --git a/framework/build.gradle b/framework/build.gradle
index fd59d3cc4e7..0ce33f253cf 100644
--- a/framework/build.gradle
+++ b/framework/build.gradle
@@ -56,6 +56,7 @@ dependencies {
}
testImplementation group: 'org.springframework', name: 'spring-test', version: "${springVersion}"
+ testImplementation group: 'javax.portlet', name: 'portlet-api', version: '3.0.1'
implementation group: 'org.zeromq', name: 'jeromq', version: '0.5.3'
api project(":chainbase")
api project(":protocol")
diff --git a/framework/src/main/java/org/tron/common/application/ApplicationImpl.java b/framework/src/main/java/org/tron/common/application/ApplicationImpl.java
index 4f31b2815ad..bab95d299ab 100644
--- a/framework/src/main/java/org/tron/common/application/ApplicationImpl.java
+++ b/framework/src/main/java/org/tron/common/application/ApplicationImpl.java
@@ -10,6 +10,7 @@
import org.tron.core.db.Manager;
import org.tron.core.net.TronNetService;
import org.tron.core.services.event.EventService;
+import org.tron.program.SolidityNode;
@Slf4j(topic = "app")
@Component
@@ -33,6 +34,9 @@ public class ApplicationImpl implements Application {
@Autowired
private ConsensusService consensusService;
+ @Autowired(required = false)
+ private SolidityNode solidityNode;
+
private final CountDownLatch shutdown = new CountDownLatch(1);
/**
@@ -50,11 +54,14 @@ public void startup() {
@Override
public void shutdown() {
this.shutdownServices();
- if (!Args.getInstance().isSolidityNode() && (!Args.getInstance().p2pDisable)) {
+ if (!Args.getInstance().isSolidityNode() && !Args.getInstance().p2pDisable) {
tronNetService.close();
}
consensusService.stop();
eventService.close();
+ if (solidityNode != null) {
+ solidityNode.close();
+ }
dbManager.close();
shutdown.countDown();
}
diff --git a/framework/src/main/java/org/tron/common/client/DatabaseGrpcClient.java b/framework/src/main/java/org/tron/common/client/DatabaseGrpcClient.java
index 0c22c264188..25f9fa60c4e 100644
--- a/framework/src/main/java/org/tron/common/client/DatabaseGrpcClient.java
+++ b/framework/src/main/java/org/tron/common/client/DatabaseGrpcClient.java
@@ -45,7 +45,7 @@ public Block getBlock(long blockNum) {
}
public void shutdown() {
- channel.shutdown();
+ channel.shutdownNow();
}
public DynamicProperties getDynamicProperties() {
diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockFilterCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockFilterCapsule.java
index 9cf3c0c690e..e0cfb6d4433 100644
--- a/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockFilterCapsule.java
+++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockFilterCapsule.java
@@ -1,7 +1,5 @@
package org.tron.common.logsfilter.capsule;
-import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.handleBLockFilter;
-
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -20,8 +18,7 @@ public class BlockFilterCapsule extends FilterTriggerCapsule {
private boolean solidified;
public BlockFilterCapsule(BlockCapsule block, boolean solidified) {
- blockHash = block.getBlockId().toString();
- this.solidified = solidified;
+ this(block.getBlockId().toString(), solidified);
}
public BlockFilterCapsule(String blockHash, boolean solidified) {
@@ -29,10 +26,4 @@ public BlockFilterCapsule(String blockHash, boolean solidified) {
this.solidified = solidified;
}
- @Override
- public void processFilterTrigger() {
- handleBLockFilter(this);
- }
-
}
-
diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/FilterTriggerCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/FilterTriggerCapsule.java
index 0280f0c96a7..5d495a5c98c 100644
--- a/framework/src/main/java/org/tron/common/logsfilter/capsule/FilterTriggerCapsule.java
+++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/FilterTriggerCapsule.java
@@ -1,8 +1,5 @@
package org.tron.common.logsfilter.capsule;
-public class FilterTriggerCapsule extends TriggerCapsule {
+public class FilterTriggerCapsule {
- public void processFilterTrigger() {
- throw new UnsupportedOperationException();
- }
}
\ No newline at end of file
diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/LogsFilterCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/LogsFilterCapsule.java
index 8a8e122d9a0..c6f35e736a3 100644
--- a/framework/src/main/java/org/tron/common/logsfilter/capsule/LogsFilterCapsule.java
+++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/LogsFilterCapsule.java
@@ -1,7 +1,5 @@
package org.tron.common.logsfilter.capsule;
-import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.handleLogsFilter;
-
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@@ -43,8 +41,4 @@ public LogsFilterCapsule(long blockNumber, String blockHash, Bloom bloom,
this.removed = removed;
}
- @Override
- public void processFilterTrigger() {
- handleLogsFilter(this);
- }
-}
\ No newline at end of file
+}
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 abd625a462a..de8b7dba1ad 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
@@ -559,6 +559,10 @@ private static void applyNodeConfig(NodeConfig nc) {
PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange();
PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics();
PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum();
+ PARAMETER.jsonRpcMaxBatchSize = jsonrpc.getMaxBatchSize();
+ PARAMETER.jsonRpcMaxResponseSize = jsonrpc.getMaxResponseSize();
+ PARAMETER.jsonRpcMaxAddressSize = jsonrpc.getMaxAddressSize();
+ PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum();
PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize();
// ---- P2P sub-bean ----
diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java
index 1d3aec2554d..a534b9d1c5d 100644
--- a/framework/src/main/java/org/tron/core/db/Manager.java
+++ b/framework/src/main/java/org/tron/core/db/Manager.java
@@ -48,6 +48,7 @@
import org.apache.commons.collections4.CollectionUtils;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.tron.api.GrpcAPI;
import org.tron.api.GrpcAPI.TransactionInfoList;
@@ -142,6 +143,7 @@
import org.tron.core.service.MortgageService;
import org.tron.core.service.RewardViCalService;
import org.tron.core.services.event.exception.EventException;
+import org.tron.core.services.jsonrpc.TronJsonRpcImpl;
import org.tron.core.store.AccountAssetStore;
import org.tron.core.store.AccountIdIndexStore;
import org.tron.core.store.AccountIndexStore;
@@ -277,6 +279,10 @@ public class Manager {
@Autowired
private RewardViCalService rewardViCalService;
+ @Lazy
+ @Autowired
+ private TronJsonRpcImpl tronJsonRpcImpl;
+
/**
* Cycle thread to rePush Transactions
*/
@@ -333,8 +339,10 @@ public class Manager {
while (isRunFilterProcessThread) {
try {
FilterTriggerCapsule filterCapsule = filterCapsuleQueue.poll(1, TimeUnit.SECONDS);
- if (filterCapsule != null) {
- filterCapsule.processFilterTrigger();
+ if (filterCapsule instanceof LogsFilterCapsule) {
+ tronJsonRpcImpl.handleLogsFilter((LogsFilterCapsule) filterCapsule);
+ } else if (filterCapsule instanceof BlockFilterCapsule) {
+ tronJsonRpcImpl.handleBLockFilter((BlockFilterCapsule) filterCapsule);
}
} catch (InterruptedException e) {
logger.error("FilterProcessLoop get InterruptedException, error is {}.",
@@ -1042,23 +1050,6 @@ public void eraseBlock() {
}
}
- public void pushVerifiedBlock(BlockCapsule block) throws ContractValidateException,
- ContractExeException, ValidateSignatureException, AccountResourceInsufficientException,
- TransactionExpirationException, TooBigTransactionException, DupTransactionException,
- TaposException, ValidateScheduleException, ReceiptCheckErrException,
- VMIllegalException, TooBigTransactionResultException, UnLinkedBlockException,
- NonCommonBlockException, BadNumberBlockException, BadBlockException, ZksnarkException,
- EventBloomException {
- block.generatedByMyself = true;
- long start = System.currentTimeMillis();
- pushBlock(block);
- logger.info("Push block cost: {} ms, blockNum: {}, blockHash: {}, trx count: {}.",
- System.currentTimeMillis() - start,
- block.getNum(),
- block.getBlockId(),
- block.getTransactions().size());
- }
-
private void applyBlock(BlockCapsule block) throws ContractValidateException,
ContractExeException, ValidateSignatureException, AccountResourceInsufficientException,
TransactionExpirationException, TooBigTransactionException, DupTransactionException,
@@ -2285,7 +2276,8 @@ private void reOrgLogsFilter() {
}
private void postBlockFilter(final BlockCapsule blockCapsule, boolean solidified) {
- BlockFilterCapsule blockFilterCapsule = new BlockFilterCapsule(blockCapsule, solidified);
+ BlockFilterCapsule blockFilterCapsule =
+ new BlockFilterCapsule(blockCapsule, solidified);
if (!filterCapsuleQueue.offer(blockFilterCapsule)) {
logger.info("Too many filters, block filter lost: {}.", blockCapsule.getBlockId());
}
diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java
index f9214f99e04..804c3fffa39 100644
--- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java
+++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java
@@ -233,6 +233,19 @@ public Message getData(Sha256Hash hash, InventoryType type) throws P2pException
}
}
+ public void pushVerifiedBlock(BlockCapsule block) throws P2pException {
+ block.generatedByMyself = true;
+ long start = System.currentTimeMillis();
+ processBlock(block, true);
+ if (!hitDown) {
+ logger.info("Push block cost: {} ms, blockNum: {}, blockHash: {}, trx count: {}.",
+ System.currentTimeMillis() - start,
+ block.getNum(),
+ block.getBlockId(),
+ block.getTransactions().size());
+ }
+ }
+
public void processBlock(BlockCapsule block, boolean isSync) throws P2pException {
if (!hitDown && dbManager.getLatestSolidityNumShutDown() > 0
&& dbManager.getLatestSolidityNumShutDown() == dbManager.getDynamicPropertiesStore()
diff --git a/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
new file mode 100644
index 00000000000..7076746b2a0
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/BufferedResponseWrapper.java
@@ -0,0 +1,178 @@
+package org.tron.core.services.filter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import lombok.Getter;
+
+/**
+ * Buffers the response body without writing to the underlying response,
+ * so the caller can replay it after the handler returns.
+ *
+ *
If {@code maxBytes > 0} and the response would exceed that limit, the
+ * {@link #isOverflow()} flag is set instead of throwing. The caller should check this flag after
+ * the handler returns and write its own error response when true.
+ *
+ *
Header-mutating methods ({@code setStatus}, {@code setContentType}) are buffered here and
+ * only forwarded to the real response via {@link #commitToResponse()}.
+ */
+public class BufferedResponseWrapper extends HttpServletResponseWrapper {
+
+ private final HttpServletResponse actual;
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ private final int maxBytes;
+ private int status = HttpServletResponse.SC_OK;
+ private String contentType;
+ private boolean committed = false;
+ @Getter
+ private volatile boolean overflow = false;
+
+ private final ServletOutputStream outputStream = new ServletOutputStream() {
+ @Override
+ public void write(int b) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() >= maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ if (overflow) {
+ return;
+ }
+ if (maxBytes > 0 && buffer.size() + len > maxBytes) {
+ markOverflow();
+ return;
+ }
+ buffer.write(b, off, len);
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+ };
+
+ private final PrintWriter writer =
+ new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true);
+
+ /**
+ * @param response the wrapped response
+ * @param maxBytes max allowed response bytes; {@code 0} means no limit
+ */
+ public BufferedResponseWrapper(HttpServletResponse response, int maxBytes) {
+ super(response);
+ this.actual = response;
+ this.maxBytes = maxBytes;
+ }
+
+ private void markOverflow() {
+ overflow = true;
+ buffer.reset();
+ }
+
+ /**
+ * Early-detection path: if the framework reports the full content length before writing any
+ * bytes, we can flag overflow without buffering anything.
+ */
+ @Override
+ public void setContentLength(int len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
+ }
+ }
+
+ @Override
+ public void setContentLengthLong(long len) {
+ if (maxBytes > 0 && len > maxBytes) {
+ markOverflow();
+ }
+ }
+
+ @Override
+ public int getStatus() {
+ return this.status;
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ this.status = sc;
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ if ("content-length".equalsIgnoreCase(name)) {
+ try {
+ setContentLengthLong(Long.parseLong(value));
+ } catch (NumberFormatException ignored) {
+ // malformed value, skip overflow check
+ }
+ } else {
+ super.setHeader(name, value);
+ }
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ if ("content-length".equalsIgnoreCase(name)) {
+ try {
+ setContentLengthLong(Long.parseLong(value));
+ } catch (NumberFormatException ignored) {
+ // malformed value, skip overflow check
+ }
+ } else {
+ super.addHeader(name, value);
+ }
+ }
+
+ @Override
+ public void setContentType(String type) {
+ this.contentType = type;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ return writer;
+ }
+
+ public void commitToResponse() throws IOException {
+ if (committed) {
+ throw new IllegalStateException("commitToResponse() already called");
+ }
+ committed = true;
+ // Flush the PrintWriter's OutputStreamWriter encoder into our ByteArrayOutputStream.
+ // PrintWriter(autoFlush=true) only auto-flushes on println/printf/format, not print/write,
+ // so bytes can remain buffered in the encoder until an explicit flush.
+ writer.flush();
+ if (overflow) {
+ return;
+ }
+ if (contentType != null) {
+ actual.setContentType(contentType);
+ }
+ actual.setStatus(status);
+ actual.setContentLength(buffer.size());
+ buffer.writeTo(actual.getOutputStream());
+ actual.getOutputStream().flush();
+ }
+}
diff --git a/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
new file mode 100644
index 00000000000..683fe849f71
--- /dev/null
+++ b/framework/src/main/java/org/tron/core/services/filter/CachedBodyRequestWrapper.java
@@ -0,0 +1,97 @@
+package org.tron.core.services.filter;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+/**
+ * Wraps a request to replay a pre-read body from a byte array,
+ * allowing the body to be read more than once.
+ *
+ *
Scope: designed for synchronous, raw-body POST endpoints
+ * (e.g. JSON-RPC). It is NOT compatible with:
+ *
+ * - {@code application/x-www-form-urlencoded} — cached body cannot back
+ * {@code getParameter*}.
+ * - multipart — {@code getPart()/getParts()} read from the original
+ * (already-consumed) stream.
+ * - async non-blocking I/O — see {@code setReadListener}.
+ * - request dispatch / forward chains.
+ *
+ *
+ * Multiple calls to {@code getInputStream()} (or {@code getReader()})
+ * are allowed and each returns a fresh stream over the same cached body —
+ * a deliberate extension of the standard servlet contract.
+ */
+public class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
+
+ private enum BodyAccessor { NONE, STREAM, READER }
+
+ private final byte[] body;
+ private BodyAccessor accessor = BodyAccessor.NONE;
+
+ public CachedBodyRequestWrapper(HttpServletRequest request, byte[] body) {
+ super(request);
+ this.body = body;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() {
+ if (accessor == BodyAccessor.READER) {
+ throw new IllegalStateException("getReader() has already been called on this request");
+ }
+ accessor = BodyAccessor.STREAM;
+ final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+ return new ServletInputStream() {
+ @Override
+ public int read() {
+ return bais.read();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) {
+ return bais.read(b, off, len);
+ }
+
+ @Override
+ public boolean isFinished() {
+ return bais.available() == 0;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ throw new UnsupportedOperationException(
+ "async I/O is not supported on cached body");
+ }
+ };
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ if (accessor == BodyAccessor.STREAM) {
+ throw new IllegalStateException("getInputStream() has already been called on this request");
+ }
+ accessor = BodyAccessor.READER;
+ String encoding = getCharacterEncoding();
+ Charset charset;
+ try {
+ charset = encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8;
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
+ charset = StandardCharsets.UTF_8;
+ }
+ return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), charset));
+ }
+}
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 104b72a66e8..6a0957d62d2 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
@@ -61,6 +61,8 @@ public class JsonRpcApiUtil {
public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported";
public static final String BLOCK_NUM_ERROR = "invalid block number";
+ private static final SecureRandom random = new SecureRandom();
+
public static byte[] convertToTronAddress(byte[] address) {
byte[] newAddress = new byte[21];
byte[] temp = new byte[] {Wallet.getAddressPreFixByte()};
@@ -647,7 +649,6 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet)
}
public static String generateFilterId() {
- SecureRandom random = new SecureRandom();
byte[] uid = new byte[16]; // 128 bits are converted to 16 bytes
random.nextBytes(uid);
return ByteArray.toHexString(uid);
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 104a0e9e470..2093930ca98 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
@@ -1,10 +1,18 @@
package org.tron.core.services.jsonrpc;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.googlecode.jsonrpc4j.HttpStatusCodeProvider;
import com.googlecode.jsonrpc4j.JsonRpcInterceptor;
import com.googlecode.jsonrpc4j.JsonRpcServer;
import com.googlecode.jsonrpc4j.ProxyUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.util.Collections;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -14,15 +22,30 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.parameter.CommonParameter;
-import org.tron.core.Wallet;
-import org.tron.core.db.Manager;
-import org.tron.core.services.NodeInfoService;
+import org.tron.core.services.filter.BufferedResponseWrapper;
+import org.tron.core.services.filter.CachedBodyRequestWrapper;
import org.tron.core.services.http.RateLimiterServlet;
@Component
@Slf4j(topic = "API")
public class JsonRpcServlet extends RateLimiterServlet {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private enum JsonRpcError {
+ PARSE_ERROR(-32700),
+ INVALID_REQUEST(-32600),
+ INTERNAL_ERROR(-32603),
+ EXCEED_LIMIT(-32005),
+ RESPONSE_TOO_LARGE(-32003);
+
+ private final int code;
+
+ JsonRpcError(int code) {
+ this.code = code;
+ }
+ }
+
private JsonRpcServer rpcServer = null;
@Autowired
@@ -66,6 +89,182 @@ public Integer getJsonRpcCode(int httpStatusCode) {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- rpcServer.handle(req, resp);
+ CommonParameter parameter = CommonParameter.getInstance();
+
+ // Transport IOException from readBody propagates as HTTP 500 (genuine IO failure).
+ byte[] body = readBody(req.getInputStream());
+ JsonNode rootNode;
+ try {
+ rootNode = MAPPER.readTree(body);
+ if (rootNode == null || rootNode.isMissingNode()) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
+ return;
+ }
+ } catch (JsonProcessingException e) {
+ writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false);
+ return;
+ }
+
+ boolean isBatch = rootNode.isArray();
+ if (isBatch && rootNode.isEmpty()) {
+ writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false);
+ return;
+ }
+ int batchSize = parameter.getJsonRpcMaxBatchSize();
+ if (isBatch && batchSize > 0 && rootNode.size() > batchSize) {
+ writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT,
+ "Batch size " + rootNode.size() + " exceeds the limit of " + batchSize, null, true);
+ return;
+ }
+
+ int maxResponseSize = parameter.getJsonRpcMaxResponseSize();
+ if (isBatch) {
+ handleBatch(resp, rootNode, maxResponseSize);
+ } else {
+ handleSingle(req, resp, rootNode, body, maxResponseSize);
+ }
+ }
+
+ private void handleSingle(HttpServletRequest req, HttpServletResponse resp,
+ JsonNode rootNode, byte[] body, int maxResponseSize) throws IOException {
+ CachedBodyRequestWrapper cachedReq = new CachedBodyRequestWrapper(req, body);
+ BufferedResponseWrapper bufferedResp = new BufferedResponseWrapper(
+ resp, maxResponseSize);
+
+ try {
+ rpcServer.handle(cachedReq, bufferedResp);
+ } catch (RuntimeException e) {
+ logger.error("RPC execution failed", e);
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error",
+ rootNode.get("id"), false);
+ return;
+ }
+
+ bufferedResp.commitToResponse();
+ if (bufferedResp.isOverflow()) {
+ writeJsonRpcError(resp, JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + maxResponseSize + " bytes",
+ rootNode.get("id"), false);
+ }
+ }
+
+ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxResponseSize)
+ throws IOException {
+
+ ArrayNode batchResult = MAPPER.createArrayNode();
+ int accumulatedSize = 2; // "[]"
+ boolean overflow = false;
+
+ for (int i = 0; i < rootNode.size(); i++) {
+ JsonNode subRequest = rootNode.get(i);
+
+ if (overflow) {
+ // Notifications (no "id") do not get a response even on overflow.
+ if (subRequest.has("id")) {
+ batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + maxResponseSize + " bytes",
+ subRequest.get("id")));
+ }
+ continue;
+ }
+
+ byte[] subBody;
+ try {
+ subBody = MAPPER.writeValueAsBytes(subRequest);
+ } catch (JsonProcessingException e) {
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+
+ ByteArrayOutputStream subOutput = new ByteArrayOutputStream();
+ try {
+ rpcServer.handleRequest(new ByteArrayInputStream(subBody), subOutput);
+ } catch (RuntimeException e) {
+ logger.error("RPC execution failed for batch sub-request {}", i, e);
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+
+ byte[] responseBytes = subOutput.toByteArray();
+ if (responseBytes.length == 0) {
+ continue; // notification — no response
+ }
+
+ // comma(,) separator between array elements
+ int addition = responseBytes.length + (!batchResult.isEmpty() ? 1 : 0);
+ if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) {
+ overflow = true;
+ batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE,
+ "Response exceeds the limit of " + maxResponseSize + " bytes",
+ subRequest.get("id")));
+ continue;
+ }
+ accumulatedSize += addition;
+
+ JsonNode responseNode;
+ try {
+ responseNode = MAPPER.readTree(responseBytes);
+ } catch (IOException e) {
+ writeJsonRpcError(resp, JsonRpcError.INTERNAL_ERROR, "Internal error", null, true);
+ return;
+ }
+ batchResult.add(responseNode);
+ }
+
+ // JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects.
+ if (batchResult.isEmpty()) {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentLength(0);
+ return;
+ }
+
+ byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult);
+ resp.setContentType("application/json-rpc; charset=utf-8");
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentLength(finalBytes.length);
+ resp.getOutputStream().write(finalBytes);
+ resp.getOutputStream().flush();
+ }
+
+ private byte[] readBody(InputStream in) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] tmp = new byte[4096];
+ int n;
+ while ((n = in.read(tmp)) != -1) {
+ buffer.write(tmp, 0, n);
+ }
+ return buffer.toByteArray();
+ }
+
+ private ObjectNode buildErrorNode(JsonRpcError error, String message, JsonNode id) {
+ ObjectNode errorObj = MAPPER.createObjectNode();
+ errorObj.put("jsonrpc", "2.0");
+ ObjectNode errNode = errorObj.putObject("error");
+ errNode.put("code", error.code);
+ errNode.put("message", message);
+ if (id != null && !id.isNull() && !id.isMissingNode()) {
+ errorObj.set("id", id);
+ } else {
+ errorObj.putNull("id");
+ }
+ return errorObj;
+ }
+
+ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, String message,
+ JsonNode id, boolean isBatch) throws IOException {
+ ObjectNode errorObj = buildErrorNode(error, message, id);
+ byte[] bytes;
+ if (isBatch) {
+ ArrayNode arr = MAPPER.createArrayNode();
+ arr.add(errorObj);
+ bytes = MAPPER.writeValueAsBytes(arr);
+ } else {
+ bytes = MAPPER.writeValueAsBytes(errorObj);
+ }
+ resp.setContentType("application/json-rpc; charset=utf-8");
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentLength(bytes.length);
+ resp.getOutputStream().write(bytes);
+ resp.getOutputStream().flush();
}
-}
\ No newline at end of file
+}
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 f5707148724..50da763b8b9 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
@@ -291,9 +291,10 @@ CompilationResult ethSubmitHashrate(String hashrate, String id)
@JsonRpcErrors({
@JsonRpcError(exception = JsonRpcMethodNotFoundException.class, code = -32601, data = "{}"),
@JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"),
+ @JsonRpcError(exception = JsonRpcExceedLimitException.class, code = -32005, data = "{}"),
})
String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
- JsonRpcMethodNotFoundException;
+ JsonRpcMethodNotFoundException, JsonRpcExceedLimitException;
@JsonRpcMethod("eth_newBlockFilter")
@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 40fafac535b..4d919b81ece 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
@@ -14,6 +14,7 @@
import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber;
import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.protobuf.ByteString;
@@ -30,6 +31,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import lombok.Getter;
@@ -118,7 +120,8 @@ public enum RequestSource {
private static final String FILTER_NOT_FOUND = "filter not found";
public static final int EXPIRE_SECONDS = 5 * 60;
- private static final int maxBlockFilterNum = Args.getInstance().getJsonRpcMaxBlockFilterNum();
+ private final int maxBlockFilterNum = Args.getInstance().getJsonRpcMaxBlockFilterNum();
+ private final int maxLogFilterNum = Args.getInstance().getJsonRpcMaxLogFilterNum();
private static final Cache logElementCache =
CacheBuilder.newBuilder()
.maximumSize(300_000L) // 300s * tps(1000) * 1 log/tx ≈ 300_000
@@ -133,25 +136,25 @@ public enum RequestSource {
* for log filter in Full Json-RPC
*/
@Getter
- private static final Map eventFilter2ResultFull =
+ private final Map eventFilter2ResultFull =
new ConcurrentHashMap<>();
/**
* for block in Full Json-RPC
*/
@Getter
- private static final Map blockFilter2ResultFull =
+ private final Map blockFilter2ResultFull =
new ConcurrentHashMap<>();
/**
* for log filter in solidity Json-RPC
*/
@Getter
- private static final Map eventFilter2ResultSolidity =
+ private final Map eventFilter2ResultSolidity =
new ConcurrentHashMap<>();
/**
* for block in solidity Json-RPC
*/
@Getter
- private static final Map blockFilter2ResultSolidity =
+ private final Map blockFilter2ResultSolidity =
new ConcurrentHashMap<>();
public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$";
@@ -169,25 +172,42 @@ public enum RequestSource {
private static final String ERROR_SELECTOR = "08c379a0"; // Function selector for Error(string)
private static final int REVERT_REASON_SELECTOR_LENGTH = 4;
private static final int MAX_REVERT_REASON_PAYLOAD_BYTES = 4096;
+ private int filterParallelThreshold = 10000;
+ /**
+ * Using the default maxLogFilterNum of 20,000, a 3-thread pool can keep up with log event
+ * processing for each block within the 3-second BLOCK_PRODUCED_INTERVAL. Increasing the thread
+ * pool size too much may affect the performance of the main block processing thread.
+ */
+ private final ForkJoinPool logsFilterPool =
+ ExecutorServiceManager.newForkJoinPool("logs-filter-pool", 3);
/**
* thread pool of query section bloom store
*/
private final ExecutorService sectionExecutor;
private final NodeInfoService nodeInfoService;
private final Wallet wallet;
- private final Manager manager;
+ @Autowired
+ private Manager manager;
private final String esName = "query-section";
@Autowired
- public TronJsonRpcImpl(@Autowired NodeInfoService nodeInfoService, @Autowired Wallet wallet,
- @Autowired Manager manager) {
+ public TronJsonRpcImpl(@Autowired NodeInfoService nodeInfoService, @Autowired Wallet wallet) {
this.nodeInfoService = nodeInfoService;
this.wallet = wallet;
- this.manager = manager;
this.sectionExecutor = ExecutorServiceManager.newFixedThreadPool(esName, 5);
}
- public static void handleBLockFilter(BlockFilterCapsule blockFilterCapsule) {
+ @VisibleForTesting
+ public void setManager(Manager manager) {
+ this.manager = manager;
+ }
+
+ @VisibleForTesting
+ public void setFilterParallelThreshold(int filterParallelThreshold) {
+ this.filterParallelThreshold = filterParallelThreshold;
+ }
+
+ public void handleBLockFilter(BlockFilterCapsule blockFilterCapsule) {
Iterator> it;
if (blockFilterCapsule.isSolidified()) {
@@ -221,54 +241,69 @@ public static void handleBLockFilter(BlockFilterCapsule blockFilterCapsule) {
/**
* append LogsFilterCapsule's LogFilterElement list to each filter if matched
*/
- public static void handleLogsFilter(LogsFilterCapsule logsFilterCapsule) {
- Iterator> it;
+ public void handleLogsFilter(LogsFilterCapsule logsFilterCapsule) {
+ long t1 = System.currentTimeMillis();
+ Map eventFilterMap;
if (logsFilterCapsule.isSolidified()) {
- it = getEventFilter2ResultSolidity().entrySet().iterator();
+ eventFilterMap = getEventFilter2ResultSolidity();
} else {
- it = getEventFilter2ResultFull().entrySet().iterator();
+ eventFilterMap = getEventFilter2ResultFull();
}
- while (it.hasNext()) {
- Entry entry = it.next();
- if (entry.getValue().isExpire()) {
- it.remove();
- continue;
- }
-
- LogFilterAndResult logFilterAndResult = entry.getValue();
- long fromBlock = logFilterAndResult.getLogFilterWrapper().getFromBlock();
- long toBlock = logFilterAndResult.getLogFilterWrapper().getToBlock();
- if (!(fromBlock <= logsFilterCapsule.getBlockNumber()
- && logsFilterCapsule.getBlockNumber() <= toBlock)) {
- continue;
- }
+ if (eventFilterMap.size() <= filterParallelThreshold) {
+ eventFilterMap.entrySet().forEach(
+ entry -> processLogFilterEntry(entry, eventFilterMap, logsFilterCapsule));
+ } else {
+ logsFilterPool.submit(() -> eventFilterMap.entrySet().parallelStream()
+ .forEach(entry -> processLogFilterEntry(entry, eventFilterMap, logsFilterCapsule))
+ ).join();
+ }
+ long t2 = System.currentTimeMillis();
+ logger.debug("handleLogsFilter {} cost {}, filter size {}",
+ logsFilterCapsule.isSolidified() ? "Solidity" : "Full", t2 - t1, eventFilterMap.size());
+ }
+
+ private void processLogFilterEntry(
+ Map.Entry entry,
+ Map eventFilterMap,
+ LogsFilterCapsule logsFilterCapsule) {
+ LogFilterAndResult logFilterAndResult = entry.getValue();
+ if (logFilterAndResult.isExpire()) {
+ eventFilterMap.remove(entry.getKey());
+ return;
+ }
- if (logsFilterCapsule.getBloom() != null
- && !logFilterAndResult.getLogFilterWrapper().getLogFilter()
- .matchBloom(logsFilterCapsule.getBloom())) {
- continue;
- }
+ long blockNumber = logsFilterCapsule.getBlockNumber();
+ long fromBlock = logFilterAndResult.getLogFilterWrapper().getFromBlock();
+ long toBlock = logFilterAndResult.getLogFilterWrapper().getToBlock();
+ if (!(fromBlock <= blockNumber && blockNumber <= toBlock)) {
+ return;
+ }
- LogFilter logFilter = logFilterAndResult.getLogFilterWrapper().getLogFilter();
- List elements =
- LogMatch.matchBlock(logFilter, logsFilterCapsule.getBlockNumber(),
- logsFilterCapsule.getBlockHash(), logsFilterCapsule.getTxInfoList(),
- logsFilterCapsule.isRemoved());
+ if (logsFilterCapsule.getBloom() != null && !logFilterAndResult.getLogFilterWrapper()
+ .getLogFilter().matchBloom(logsFilterCapsule.getBloom())) {
+ return;
+ }
- for (LogFilterElement element : elements) {
- LogFilterElement cachedElement;
- try {
- // compare with hashcode() first, then with equals(). If not exist, put it.
- cachedElement = logElementCache.get(element, () -> element);
- } catch (ExecutionException e) {
- logger.error("Getting/loading LogFilterElement from cache fails", e); // never happen
- cachedElement = element;
- }
- logFilterAndResult.getResult().add(cachedElement);
+ LogFilter logFilter = logFilterAndResult.getLogFilterWrapper().getLogFilter();
+ List elements =
+ LogMatch.matchBlock(logFilter, blockNumber, logsFilterCapsule.getBlockHash(),
+ logsFilterCapsule.getTxInfoList(), logsFilterCapsule.isRemoved());
+
+ List localResults = new ArrayList<>(elements.size());
+ for (LogFilterElement element : elements) {
+ LogFilterElement cachedElement;
+ try {
+ // compare with hashcode() first, then with equals(). If not exist, put it.
+ cachedElement = logElementCache.get(element, () -> element);
+ } catch (ExecutionException e) {
+ logger.error("Getting/loading LogFilterElement from cache fails", e); // never happen
+ cachedElement = element;
}
+ localResults.add(cachedElement);
}
+ logFilterAndResult.getResult().addAll(localResults);
}
@Override
@@ -1399,7 +1434,7 @@ public CompilationResult ethSubmitHashrate(String hashrate, String id)
@Override
public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
- JsonRpcMethodNotFoundException {
+ JsonRpcMethodNotFoundException, JsonRpcExceedLimitException {
disableInPBFT("eth_newFilter");
// not supports finalized as block parameter
@@ -1414,7 +1449,11 @@ public String newFilter(FilterRequest fr) throws JsonRpcInvalidParamsException,
} else {
eventFilter2Result = eventFilter2ResultSolidity;
}
-
+ // Due to concurrent access, the threshold may occasionally be exceeded.
+ if (maxLogFilterNum > 0 && eventFilter2Result.size() >= maxLogFilterNum) {
+ throw new JsonRpcExceedLimitException(
+ "exceed max log filters: " + maxLogFilterNum + ", try again later");
+ }
long currentMaxFullNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber();
LogFilterAndResult logFilterAndResult = new LogFilterAndResult(fr, currentMaxFullNum, wallet);
String filterID = generateFilterId();
@@ -1433,7 +1472,7 @@ public String newBlockFilter() throws JsonRpcMethodNotFoundException,
} else {
blockFilter2Result = blockFilter2ResultSolidity;
}
- if (blockFilter2Result.size() >= maxBlockFilterNum) {
+ if (maxBlockFilterNum > 0 && blockFilter2Result.size() >= maxBlockFilterNum) {
throw new JsonRpcExceedLimitException(
"exceed max block filters: " + maxBlockFilterNum + ", try again later");
}
@@ -1542,7 +1581,7 @@ private LogFilterElement[] getLogsByLogFilterWrapper(LogFilterWrapper logFilterW
return logMatch.matchBlockOneByOne();
}
- public static Object[] getFilterResult(String filterId, Map
+ public Object[] getFilterResult(String filterId, Map
blockFilter2Result, Map eventFilter2Result)
throws ItemNotFoundException {
Object[] result;
@@ -1566,6 +1605,7 @@ public static Object[] getFilterResult(String filterId, Map 0 && ((ArrayList>) fr.getAddress()).size() > maxAddressSize) {
+ throw new JsonRpcInvalidParamsException("exceed max addresses: " + maxAddressSize);
+ }
List addr = new ArrayList<>();
int i = 0;
for (Object s : (ArrayList) fr.getAddress()) {
diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java
index 67d229b2948..04994969fc4 100644
--- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java
+++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java
@@ -94,14 +94,14 @@ public LogFilterElement[] matchBlockOneByOne()
String blockHash = manager.getChainBaseManager().getBlockIdByNum(blockNum).toString();
List matchedLog = matchBlock(logFilterWrapper.getLogFilter(), blockNum,
blockHash, transactionInfoList, false);
+
if (!matchedLog.isEmpty()) {
+ if (logFilterElementList.size() + matchedLog.size() > LogBlockQuery.MAX_RESULT) {
+ throw new JsonRpcTooManyResultException(
+ "query returned more than " + LogBlockQuery.MAX_RESULT + " results");
+ }
logFilterElementList.addAll(matchedLog);
}
-
- if (logFilterElementList.size() > LogBlockQuery.MAX_RESULT) {
- throw new JsonRpcTooManyResultException(
- "query returned more than " + LogBlockQuery.MAX_RESULT + " results");
- }
}
return logFilterElementList.toArray(new LogFilterElement[0]);
diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java
index 95257d77f8e..308cb9a1c69 100644
--- a/framework/src/main/java/org/tron/program/FullNode.java
+++ b/framework/src/main/java/org/tron/program/FullNode.java
@@ -2,6 +2,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.util.ObjectUtils;
import org.tron.common.application.Application;
import org.tron.common.application.ApplicationFactory;
import org.tron.common.application.TronApplicationContext;
@@ -28,19 +29,23 @@ public static void main(String[] args) {
LogService.load(parameter.getLogbackPath());
- if (parameter.isSolidityNode()) {
- SolidityNode.start();
- return;
- }
if (parameter.isKeystoreFactory()) {
KeystoreFactory.start();
return;
}
- logger.info("Full node running.");
- if (Args.getInstance().isDebug()) {
- logger.info("in debug mode, it won't check energy time");
+ if (parameter.isSolidityNode()) {
+ logger.info("Solidity node is running.");
+ if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) {
+ throw new TronError(new IllegalArgumentException("Trust node is not set."),
+ TronError.ErrCode.SOLID_NODE_INIT);
+ }
} else {
- logger.info("not in debug mode, it will check energy time");
+ logger.info("Full node running.");
+ if (Args.getInstance().isDebug()) {
+ logger.info("in debug mode, it won't check energy time");
+ } else {
+ logger.info("not in debug mode, it will check energy time");
+ }
}
// init metrics first
@@ -55,6 +60,10 @@ public static void main(String[] args) {
Application appT = ApplicationFactory.create(context);
context.registerShutdownHook();
appT.startup();
+ if (parameter.isSolidityNode()) {
+ SolidityNode node = context.getBean(SolidityNode.class);
+ node.run();
+ }
appT.blockUntilShutdown();
}
diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java
index 6ffa3b3ce92..0998d8846c0 100644
--- a/framework/src/main/java/org/tron/program/SolidityNode.java
+++ b/framework/src/main/java/org/tron/program/SolidityNode.java
@@ -1,227 +1,233 @@
-package org.tron.program;
-
-import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL;
-
-import com.google.common.annotations.VisibleForTesting;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.support.DefaultListableBeanFactory;
-import org.springframework.util.ObjectUtils;
-import org.tron.common.application.Application;
-import org.tron.common.application.ApplicationFactory;
-import org.tron.common.application.TronApplicationContext;
-import org.tron.common.client.DatabaseGrpcClient;
-import org.tron.common.es.ExecutorServiceManager;
-import org.tron.common.parameter.CommonParameter;
-import org.tron.common.prometheus.Metrics;
-import org.tron.core.ChainBaseManager;
-import org.tron.core.capsule.BlockCapsule;
-import org.tron.core.config.DefaultConfig;
-import org.tron.core.db.Manager;
-import org.tron.core.exception.TronError;
-import org.tron.protos.Protocol.Block;
-
-@Slf4j(topic = "app")
-public class SolidityNode {
-
- private Manager dbManager;
-
- private ChainBaseManager chainBaseManager;
-
- private DatabaseGrpcClient databaseGrpcClient;
-
- private AtomicLong ID = new AtomicLong();
-
- private AtomicLong remoteBlockNum = new AtomicLong();
-
- private LinkedBlockingDeque blockQueue = new LinkedBlockingDeque<>(100);
-
- private int exceptionSleepTime = 1000;
-
- private volatile boolean flag = true;
-
- private ExecutorService getBlockEs;
- private ExecutorService processBlockEs;
-
- public SolidityNode(Manager dbManager) {
- this.dbManager = dbManager;
- this.chainBaseManager = dbManager.getChainBaseManager();
- resolveCompatibilityIssueIfUsingFullNodeDatabase();
- ID.set(chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum());
- databaseGrpcClient = new DatabaseGrpcClient(CommonParameter.getInstance().getTrustNodeAddr());
- remoteBlockNum.set(getLastSolidityBlockNum());
- }
-
- /**
- * Start the SolidityNode.
- */
- public static void start() {
- logger.info("Solidity node is running.");
- CommonParameter parameter = CommonParameter.getInstance();
- if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) {
- throw new TronError(new IllegalArgumentException("Trust node is not set."),
- TronError.ErrCode.SOLID_NODE_INIT);
- }
- // init metrics first
- Metrics.init();
-
- DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
- beanFactory.setAllowCircularReferences(false);
- TronApplicationContext context =
- new TronApplicationContext(beanFactory);
- context.register(DefaultConfig.class);
- context.refresh();
- Application appT = ApplicationFactory.create(context);
- context.registerShutdownHook();
- appT.startup();
- SolidityNode node = new SolidityNode(appT.getDbManager());
- node.run();
- awaitShutdown(appT, node);
- }
-
- @VisibleForTesting
- static void awaitShutdown(Application appT, SolidityNode node) {
- try {
- appT.blockUntilShutdown();
- } finally {
- // SolidityNode is created manually rather than managed by Spring/Application,
- // so its executors must be shut down explicitly on exit.
- node.shutdown();
- }
- }
-
- private void run() {
- try {
- getBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-get-block");
- processBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-process-block");
- getBlockEs.execute(this::getBlock);
- processBlockEs.execute(this::processBlock);
- logger.info("Success to start solid node, ID: {}, remoteBlockNum: {}.", ID.get(),
- remoteBlockNum);
- } catch (Exception e) {
- logger.error("Failed to start solid node, address: {}.",
- CommonParameter.getInstance().getTrustNodeAddr());
- throw new TronError(e, TronError.ErrCode.SOLID_NODE_INIT);
- }
- }
-
- public void shutdown() {
- flag = false;
- // Signal both pools before awaiting either so they drain concurrently
- getBlockEs.shutdown();
- processBlockEs.shutdown();
- ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "solid-get-block");
- ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "solid-process-block");
- }
-
- private void getBlock() {
- long blockNum = ID.incrementAndGet();
- while (flag) {
- try {
- if (blockNum > remoteBlockNum.get()) {
- sleep(BLOCK_PRODUCED_INTERVAL);
- remoteBlockNum.set(getLastSolidityBlockNum());
- continue;
- }
- Block block = getBlockByNum(blockNum);
- blockQueue.put(block);
- blockNum = ID.incrementAndGet();
- } catch (Exception e) {
- logger.error("Failed to get block {}, reason: {}.", blockNum, e.getMessage());
- sleep(exceptionSleepTime);
- }
- }
- }
-
- private void processBlock() {
- while (flag) {
- try {
- Block block = blockQueue.take();
- loopProcessBlock(block);
- } catch (Exception e) {
- logger.error(e.getMessage());
- sleep(exceptionSleepTime);
- }
- }
- }
-
- private void loopProcessBlock(Block block) {
- while (flag) {
- long blockNum = block.getBlockHeader().getRawData().getNumber();
- try {
- dbManager.pushVerifiedBlock(new BlockCapsule(block));
- chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(blockNum);
- logger
- .info("Success to process block: {}, blockQueueSize: {}.", blockNum, blockQueue.size());
- return;
- } catch (Exception e) {
- logger.error("Failed to process block {}.", new BlockCapsule(block), e);
- sleep(exceptionSleepTime);
- block = getBlockByNum(blockNum);
- }
- }
- }
-
- private Block getBlockByNum(long blockNum) {
- while (flag) {
- try {
- long time = System.currentTimeMillis();
- Block block = databaseGrpcClient.getBlock(blockNum);
- long num = block.getBlockHeader().getRawData().getNumber();
- if (num == blockNum) {
- logger.info("Success to get block: {}, cost: {}ms.",
- blockNum, System.currentTimeMillis() - time);
- return block;
- } else {
- logger.warn("Get block id not the same , {}, {}.", num, blockNum);
- sleep(exceptionSleepTime);
- }
- } catch (Exception e) {
- logger.error("Failed to get block: {}, reason: {}.", blockNum, e.getMessage());
- sleep(exceptionSleepTime);
- }
- }
- return null;
- }
-
- private long getLastSolidityBlockNum() {
- while (flag) {
- try {
- long time = System.currentTimeMillis();
- long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum();
- logger.info("Get last remote solid blockNum: {}, remoteBlockNum: {}, cost: {}.",
- blockNum, remoteBlockNum, System.currentTimeMillis() - time);
- return blockNum;
- } catch (Exception e) {
- logger.error("Failed to get last solid blockNum: {}, reason: {}.", remoteBlockNum.get(),
- e.getMessage());
- sleep(exceptionSleepTime);
- }
- }
- return 0;
- }
-
- public void sleep(long time) {
- try {
- Thread.sleep(time);
- } catch (Exception e1) {
- logger.error(e1.getMessage());
- }
- }
-
- private void resolveCompatibilityIssueIfUsingFullNodeDatabase() {
- long lastSolidityBlockNum =
- chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum();
- long headBlockNum = chainBaseManager.getHeadBlockNum();
- logger.info("headBlockNum:{}, solidityBlockNum:{}, diff:{}",
- headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum);
- if (lastSolidityBlockNum < headBlockNum) {
- logger.info("use fullNode database, headBlockNum:{}, solidityBlockNum:{}, diff:{}",
- headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum);
- chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum);
- }
- }
-}
+package org.tron.program;
+
+import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.stereotype.Component;
+import org.tron.common.client.DatabaseGrpcClient;
+import org.tron.common.es.ExecutorServiceManager;
+import org.tron.common.parameter.CommonParameter;
+import org.tron.core.ChainBaseManager;
+import org.tron.core.capsule.BlockCapsule;
+import org.tron.core.config.args.Args;
+import org.tron.core.exception.TronError;
+import org.tron.core.net.TronNetDelegate;
+import org.tron.protos.Protocol.Block;
+
+@Slf4j(topic = "app")
+@Conditional(SolidityNode.SolidityCondition.class)
+@Component
+public class SolidityNode implements ApplicationListener {
+
+ @Autowired
+ private ChainBaseManager chainBaseManager;
+
+ @Autowired
+ private TronNetDelegate tronNetDelegate;
+
+ private DatabaseGrpcClient databaseGrpcClient;
+
+ private final AtomicLong ID = new AtomicLong();
+
+ private final AtomicLong remoteBlockNum = new AtomicLong();
+
+ private final LinkedBlockingDeque blockQueue = new LinkedBlockingDeque<>(100);
+
+ private final int exceptionSleepTime = 1000;
+
+ private volatile boolean flag = true;
+
+ private final String getBlockName = "get-block";
+ private final String processBlockName = "process-block";
+
+ private ExecutorService getBlockExecutor;
+ private ExecutorService processBlockExecutor;
+
+ @PostConstruct
+ private void init() {
+ resolveCompatibilityIssueIfUsingFullNodeDatabase();
+ ID.set(chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum());
+ getBlockExecutor = ExecutorServiceManager.newSingleThreadExecutor(getBlockName);
+ processBlockExecutor = ExecutorServiceManager.newSingleThreadExecutor(processBlockName);
+ }
+
+ @Override
+ public void onApplicationEvent(ContextClosedEvent event) {
+ flag = false; // invoke earlier than @PreDestroy
+ }
+
+ public void close() {
+ flag = false;
+ if (databaseGrpcClient != null) {
+ databaseGrpcClient.shutdown();
+ }
+ // Interrupt get-block immediately: it may be stuck in blockQueue.put() (full queue,
+ // process-block stopped) or in a gRPC blocking stub call.
+ // Do NOT interrupt process-block: let it finish its current pushVerifiedBlock naturally
+ // (flag=false causes the while-loop to exit within 1-2 s) so the DB flush can complete
+ // cleanly before ApplicationImpl.shutdown() tears down the underlying executor.
+ getBlockExecutor.shutdownNow();
+ ExecutorServiceManager.shutdownAndAwaitTermination(getBlockExecutor, getBlockName);
+ ExecutorServiceManager.shutdownAndAwaitTermination(processBlockExecutor, processBlockName);
+ }
+
+ @PreDestroy
+ private void shutdown() {
+ close();
+ }
+
+ public void run() {
+ try {
+ databaseGrpcClient = new DatabaseGrpcClient(CommonParameter.getInstance().getTrustNodeAddr());
+ remoteBlockNum.set(getLastSolidityBlockNum());
+
+ getBlockExecutor.submit(this::getBlock);
+ processBlockExecutor.submit(this::processSolidityBlock);
+ logger.info("Success to start solid node, ID: {}, remoteBlockNum: {}.", ID.get(),
+ remoteBlockNum);
+ } catch (Exception e) {
+ logger.error("Failed to start solid node, address: {}.",
+ CommonParameter.getInstance().getTrustNodeAddr());
+ throw new TronError(e, TronError.ErrCode.SOLID_NODE_INIT);
+ }
+ }
+
+ private void getBlock() {
+ long blockNum = ID.incrementAndGet();
+ while (flag && !tronNetDelegate.isHitDown()) {
+ try {
+ if (blockNum > remoteBlockNum.get()) {
+ sleep(BLOCK_PRODUCED_INTERVAL);
+ remoteBlockNum.set(getLastSolidityBlockNum());
+ continue;
+ }
+ Block block = getBlockByNum(blockNum);
+ blockQueue.put(block);
+ blockNum = ID.incrementAndGet();
+ } catch (Exception e) {
+ logger.error("Failed to get block {}, reason: {}.", blockNum, e.getMessage());
+ sleep(exceptionSleepTime);
+ }
+ }
+ }
+
+ private void processSolidityBlock() {
+ while (flag && !tronNetDelegate.isHitDown()) {
+ try {
+ Block block = blockQueue.poll(exceptionSleepTime, TimeUnit.MILLISECONDS);
+ if (block == null) {
+ continue;
+ }
+ loopProcessBlock(block);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.info("processSolidityBlock interrupted.");
+ return;
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ sleep(exceptionSleepTime);
+ }
+ }
+ }
+
+ private void loopProcessBlock(Block block) {
+ while (flag) {
+ long blockNum = block.getBlockHeader().getRawData().getNumber();
+ try {
+ tronNetDelegate.pushVerifiedBlock(new BlockCapsule(block));
+ if (!tronNetDelegate.isHitDown()) {
+ chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(blockNum);
+ logger.info("Success to process block: {}, blockQueueSize: {}.",
+ blockNum, blockQueue.size());
+ }
+ return;
+ } catch (Exception e) {
+ logger.error("Failed to process block {}.", new BlockCapsule(block), e);
+ sleep(exceptionSleepTime);
+ block = getBlockByNum(blockNum);
+ }
+ }
+ }
+
+ private Block getBlockByNum(long blockNum) {
+ while (flag && !tronNetDelegate.isHitDown()) {
+ try {
+ long time = System.currentTimeMillis();
+ Block block = databaseGrpcClient.getBlock(blockNum);
+ long num = block.getBlockHeader().getRawData().getNumber();
+ if (num == blockNum) {
+ logger.info("Success to get block: {}, cost: {}ms.",
+ blockNum, System.currentTimeMillis() - time);
+ return block;
+ } else {
+ logger.warn("Get block id not the same , {}, {}.", num, blockNum);
+ sleep(exceptionSleepTime);
+ }
+ } catch (Exception e) {
+ logger.error("Failed to get block: {}, reason: {}.", blockNum, e.getMessage());
+ sleep(exceptionSleepTime);
+ }
+ }
+ //throw RuntimeException instead of return null to avoid NullPointException
+ throw new RuntimeException("SolidityNode is closing.");
+ }
+
+ private long getLastSolidityBlockNum() {
+ while (flag && !tronNetDelegate.isHitDown()) {
+ try {
+ long time = System.currentTimeMillis();
+ long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum();
+ logger.info("Get last remote solid blockNum: {}, remoteBlockNum: {}, cost: {}.",
+ blockNum, remoteBlockNum, System.currentTimeMillis() - time);
+ return blockNum;
+ } catch (Exception e) {
+ logger.error("Failed to get last solid blockNum: {}, reason: {}.", remoteBlockNum.get(),
+ e.getMessage());
+ sleep(exceptionSleepTime);
+ }
+ }
+ return 0;
+ }
+
+ public void sleep(long time) {
+ try {
+ Thread.sleep(time);
+ } catch (Exception e1) {
+ logger.error(e1.getMessage());
+ }
+ }
+
+ private void resolveCompatibilityIssueIfUsingFullNodeDatabase() {
+ long lastSolidityBlockNum =
+ chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum();
+ long headBlockNum = chainBaseManager.getHeadBlockNum();
+ logger.info("headBlockNum:{}, solidityBlockNum:{}, diff:{}",
+ headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum);
+ if (lastSolidityBlockNum < headBlockNum) {
+ logger.info("use fullNode database, headBlockNum:{}, solidityBlockNum:{}, diff:{}",
+ headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum);
+ chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum);
+ }
+ }
+
+ static class SolidityCondition implements Condition {
+
+ @Override
+ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ return Args.getInstance().isSolidityNode();
+ }
+ }
+}
diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf
index 7a395e88159..b180ecd6d10 100644
--- a/framework/src/main/resources/config.conf
+++ b/framework/src/main/resources/config.conf
@@ -388,15 +388,22 @@ node {
# httpPBFTEnable = false
# httpPBFTPort = 8565
- # The maximum blocks range to retrieve logs for eth_getLogs, default value is 5000,
- # should be > 0, otherwise means no limit.
+ # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit
maxBlockRange = 5000
-
- # The maximum number of allowed topics within a topic criteria, default value is 1000,
- # should be > 0, otherwise means no limit.
+ # Allowed max address count in filter request, default: 1000, <=0 means no limit
+ maxAddressSize = 1000
+ # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit
maxSubTopics = 1000
- # Allowed maximum number for blockFilter
+ # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit
maxBlockFilterNum = 50000
+ # Allowed batch size, default: 100, <=0 means no limit
+ maxBatchSize = 100
+ # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit
+ maxResponseSize = 26214400
+ # Allowed maximum number for newFilter, <=0 means no limit
+ maxLogFilterNum = 20000
+ # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize.
+ maxMessageSize = 4M
}
# Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode,
diff --git a/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java b/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java
index c87d8e1136e..b57b3a92fcd 100644
--- a/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java
+++ b/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java
@@ -21,7 +21,6 @@
import org.junit.Assert;
import org.junit.Test;
import org.tron.common.logsfilter.capsule.ContractEventTriggerCapsule;
-import org.tron.common.logsfilter.capsule.FilterTriggerCapsule;
import org.tron.common.logsfilter.capsule.TriggerCapsule;
import org.tron.common.runtime.LogEventWrapper;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry;
@@ -103,13 +102,6 @@ public synchronized void testMatchFilter() {
assertNotNull(filterQuery.toString());
}
- FilterTriggerCapsule filterTriggerCapsule = new FilterTriggerCapsule();
- try {
- filterTriggerCapsule.processFilterTrigger();
- } catch (Exception e) {
- logger.info(e.getMessage());
- }
-
TriggerCapsule triggerCapsule = new TriggerCapsule();
try {
triggerCapsule.processTrigger();
diff --git a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java
index b5f7e676eea..aac42facf96 100644
--- a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java
+++ b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java
@@ -31,7 +31,6 @@ public void testSetAndIsSolidified() {
blockFilterCapsule = new BlockFilterCapsule(
"e58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f", false);
blockFilterCapsule.setSolidified(true);
- blockFilterCapsule.processFilterTrigger();
Assert.assertTrue(blockFilterCapsule.isSolidified());
}
}
diff --git a/framework/src/test/java/org/tron/common/logsfilter/capsule/LogsFilterCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/capsule/LogsFilterCapsuleTest.java
index 691a3106b49..f23c446c23d 100644
--- a/framework/src/test/java/org/tron/common/logsfilter/capsule/LogsFilterCapsuleTest.java
+++ b/framework/src/test/java/org/tron/common/logsfilter/capsule/LogsFilterCapsuleTest.java
@@ -27,7 +27,6 @@ public void testSetAndGetLogsFilterCapsule() {
capsule.setRemoved(capsule.isRemoved());
capsule.setTxInfoList(capsule.getTxInfoList());
assertNotNull(capsule.toString());
- capsule.processFilterTrigger();
}
}
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/Create2Test.java b/framework/src/test/java/org/tron/common/runtime/vm/Create2Test.java
index 6fa2801c51f..5a58407f887 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/Create2Test.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/Create2Test.java
@@ -209,7 +209,8 @@ private void testJsonRpc(byte[] actualContract, long loop) {
NodeInfoService nodeInfoService;
nodeInfoService = context.getBean(NodeInfoService.class);
Wallet wallet = context.getBean(Wallet.class);
- tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, manager);
+ tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(manager);
try {
String res =
tronJsonRpc.getStorageAt(ByteArray.toHexString(actualContract), "0", "latest");
diff --git a/framework/src/test/java/org/tron/core/db/ManagerTest.java b/framework/src/test/java/org/tron/core/db/ManagerTest.java
index eb0f8a7d5bf..87b4fcfdc77 100755
--- a/framework/src/test/java/org/tron/core/db/ManagerTest.java
+++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java
@@ -319,12 +319,6 @@ public void transactionTest() {
} catch (Exception e) {
Assert.assertTrue(e instanceof TaposException);
}
- try {
- dbManager.pushVerifiedBlock(chainManager.getHead());
- dbManager.getBlockChainHashesOnFork(chainManager.getHeadBlockId());
- } catch (Exception e) {
- Assert.assertTrue(e instanceof TaposException);
- }
}
@Test
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java b/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java
index 2cdcaaf7a53..2fcb624002e 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java
@@ -23,6 +23,7 @@
@Slf4j
public class ConcurrentHashMapTest {
private static final String EXECUTOR_NAME = "jsonrpc-concurrent-map-test";
+ private final TronJsonRpcImpl jsonRpc = new TronJsonRpcImpl(null, null);
private static int randomInt(int minInt, int maxInt) {
return (int) round(random(true) * (maxInt - minInt) + minInt, true);
@@ -39,7 +40,7 @@ public void testHandleBlockHash() {
int times = 100;
int eachCount = 200;
- Map conMap = TronJsonRpcImpl.getBlockFilter2ResultFull();
+ Map conMap = jsonRpc.getBlockFilter2ResultFull();
Map> resultMap1 = new ConcurrentHashMap<>(); // used to check result
Map> resultMap2 = new ConcurrentHashMap<>(); // used to check result
Map> resultMap3 = new ConcurrentHashMap<>(); // used to check result
@@ -71,7 +72,7 @@ public void testHandleBlockHash() {
for (int j = 1 + (i - 1) * eachCount; j <= i * eachCount; j++) {
BlockFilterCapsule blockFilterCapsule =
new BlockFilterCapsule(String.valueOf(j), false);
- TronJsonRpcImpl.handleBLockFilter(blockFilterCapsule);
+ jsonRpc.handleBLockFilter(blockFilterCapsule);
}
try {
Thread.sleep(randomInt(50, 100));
@@ -96,8 +97,8 @@ public void testHandleBlockHash() {
for (int k = 0; k < 5; k++) {
try {
- Object[] blockHashList = TronJsonRpcImpl.getFilterResult(String.valueOf(k), conMap,
- TronJsonRpcImpl.getEventFilter2ResultFull());
+ Object[] blockHashList = jsonRpc.getFilterResult(String.valueOf(k), conMap,
+ jsonRpc.getEventFilter2ResultFull());
for (Object str : blockHashList) {
resultMap1.get(String.valueOf(k)).add(str.toString());
@@ -124,8 +125,8 @@ public void testHandleBlockHash() {
for (int k = 0; k < 5; k++) {
try {
- Object[] blockHashList = TronJsonRpcImpl.getFilterResult(String.valueOf(k), conMap,
- TronJsonRpcImpl.getEventFilter2ResultFull());
+ Object[] blockHashList = jsonRpc.getFilterResult(String.valueOf(k), conMap,
+ jsonRpc.getEventFilter2ResultFull());
// if (blockHashList.length == 0) {
// continue;
@@ -156,8 +157,8 @@ public void testHandleBlockHash() {
for (int k = 0; k < 5; k++) {
try {
- Object[] blockHashList = TronJsonRpcImpl.getFilterResult(String.valueOf(k), conMap,
- TronJsonRpcImpl.getEventFilter2ResultFull());
+ Object[] blockHashList = jsonRpc.getFilterResult(String.valueOf(k), conMap,
+ jsonRpc.getEventFilter2ResultFull());
for (Object str : blockHashList) {
try {
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java
new file mode 100644
index 00000000000..33835c482fe
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/jsonrpc/HandleLogsFilterTest.java
@@ -0,0 +1,293 @@
+package org.tron.core.jsonrpc;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+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;
+import org.tron.core.services.jsonrpc.filters.LogFilterAndResult;
+import org.tron.protos.Protocol.TransactionInfo;
+
+public class HandleLogsFilterTest {
+
+ private static final String FILTER_ID_1 = "handle-logs-test-001";
+ private static final String FILTER_ID_2 = "handle-logs-test-002";
+
+ private TronJsonRpcImpl jsonRpc;
+
+ @Before
+ public void setUp() {
+ jsonRpc = new TronJsonRpcImpl(null, null);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ jsonRpc.close();
+ }
+
+ private TransactionInfo buildTxInfoWithLog(byte[] address) {
+ LogInfo logInfo = new LogInfo(address,
+ Collections.singletonList(new DataWord(new byte[32])), new byte[0]);
+ return TransactionInfo.newBuilder().addLog(LogInfo.buildLog(logInfo)).build();
+ }
+
+ /**
+ * Events dispatched to a matching filter in the serial (<=10000 entries) path.
+ */
+ @Test
+ public void testMatchingFilter_receivesLogElements() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult);
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertEquals(1, filterAndResult.getResult().size());
+ }
+
+ /**
+ * Filter with fromBlock=100 does not receive a capsule whose blockNumber is 50.
+ */
+ @Test
+ public void testBlockNumberBelowRange_noResult() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ // currentMaxBlockNum=100 → fromBlock=100, toBlock=MAX_VALUE
+ LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult);
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(50L, "0xabcdef", null, txInfoList, false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertTrue(filterAndResult.getResult().isEmpty());
+ }
+
+ /**
+ * An expired filter is removed from the map during handleLogsFilter.
+ */
+ @Test
+ public void testExpiredFilter_removedFromMap() throws Exception {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
+
+ Field expireField = FilterResult.class.getDeclaredField("expireTimeStamp");
+ expireField.setAccessible(true);
+ expireField.setLong(filterAndResult, 0L);
+
+ Map map = jsonRpc.getEventFilter2ResultFull();
+ map.put(FILTER_ID_1, filterAndResult);
+ Assert.assertTrue(map.containsKey(FILTER_ID_1));
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertFalse("expired filter should be removed", map.containsKey(FILTER_ID_1));
+ }
+
+ /**
+ * A solidified capsule is routed only to the solidity map; the full-node map is untouched.
+ */
+ @Test
+ public void testSolidifiedCapsule_routedToSolidityMap() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter);
+
+ LogFilterAndResult fullFilter = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_2, fullFilter);
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, true, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertEquals(1, solidityFilter.getResult().size());
+ Assert.assertTrue("full-node filter must not be touched", fullFilter.getResult().isEmpty());
+ }
+
+ /**
+ * A non-solidified capsule is routed only to the full-node map.
+ */
+ @Test
+ public void testNonSolidifiedCapsule_routedToFullMap() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult solidityFilter = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultSolidity().put(FILTER_ID_1, solidityFilter);
+
+ LogFilterAndResult fullFilter = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_2, fullFilter);
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertEquals(1, fullFilter.getResult().size());
+ Assert.assertTrue("solidity filter must not be touched", solidityFilter.getResult().isEmpty());
+ }
+
+ /**
+ * Both filters in the map receive events when both match.
+ */
+ @Test
+ public void testMultipleMatchingFilters_bothReceiveEvents() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult filter1 = new LogFilterAndResult(fr, 100L, null);
+ LogFilterAndResult filter2 = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filter1);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_2, filter2);
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertEquals(1, filter1.getResult().size());
+ Assert.assertEquals(1, filter2.getResult().size());
+ }
+
+ /**
+ * An empty txInfoList produces no results.
+ */
+ @Test
+ public void testEmptyTxInfoList_noResult() throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest();
+ LogFilterAndResult filterAndResult = new LogFilterAndResult(fr, 100L, null);
+ jsonRpc.getEventFilter2ResultFull().put(FILTER_ID_1, filterAndResult);
+
+ LogsFilterCapsule capsule = new LogsFilterCapsule(150L, "0xabcdef", null,
+ Collections.emptyList(), false, false);
+
+ jsonRpc.handleLogsFilter(capsule);
+
+ Assert.assertTrue(filterAndResult.getResult().isEmpty());
+ }
+
+ private void setParallelThreshold(int value) {
+ jsonRpc.setFilterParallelThreshold(value);
+ }
+
+ /**
+ * Parallel path: every matching filter receives exactly one event — no events dropped or
+ * double-counted under concurrent dispatch.
+ */
+ @Test(timeout = 10000)
+ public void testParallelPath_allMatchingFilters_receiveEvents() throws Exception {
+ setParallelThreshold(2);
+ int count = 5;
+ FilterRequest fr = new FilterRequest();
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ Map map = jsonRpc.getEventFilter2ResultFull();
+ String prefix = "parallel-match-";
+ for (int i = 0; i < count; i++) {
+ map.put(prefix + i, new LogFilterAndResult(fr, 0L, null));
+ }
+
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+ jsonRpc.handleLogsFilter(capsule);
+
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals("filter " + i + " must receive exactly one event",
+ 1, map.get(prefix + i).getResult().size());
+ }
+ }
+
+ /**
+ * Parallel path: expired filters are evicted and all valid filters still receive their events.
+ */
+ @Test(timeout = 10000)
+ public void testParallelPath_expiredFiltersRemoved() throws Exception {
+ setParallelThreshold(2);
+ int expiredCount = 2;
+ int validCount = 3;
+ FilterRequest fr = new FilterRequest();
+ Field expireField = FilterResult.class.getDeclaredField("expireTimeStamp");
+ expireField.setAccessible(true);
+ Map map = jsonRpc.getEventFilter2ResultFull();
+ String prefix = "parallel-expire-";
+ for (int i = 0; i < expiredCount + validCount; i++) {
+ LogFilterAndResult filter = new LogFilterAndResult(fr, 0L, null);
+ if (i < expiredCount) {
+ expireField.setLong(filter, 0L);
+ }
+ map.put(prefix + i, filter);
+ }
+
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, false, false);
+ jsonRpc.handleLogsFilter(capsule);
+
+ for (int i = 0; i < expiredCount; i++) {
+ Assert.assertFalse("expired filter " + i + " should be removed",
+ map.containsKey(prefix + i));
+ }
+ for (int i = expiredCount; i < expiredCount + validCount; i++) {
+ Assert.assertEquals("valid filter " + i + " must receive one event",
+ 1, map.get(prefix + i).getResult().size());
+ }
+ }
+
+ /**
+ * Parallel path: a solidified capsule dispatches only to the solidity map; the full-node map
+ * is untouched even though it holds entries.
+ */
+ @Test(timeout = 10000)
+ public void testParallelPath_solidifiedCapsule_routedToSolidityMap() throws Exception {
+ setParallelThreshold(2);
+ int count = 5;
+ FilterRequest fr = new FilterRequest();
+ List txInfoList =
+ Collections.singletonList(buildTxInfoWithLog(new byte[20]));
+ Map solidityMap = jsonRpc.getEventFilter2ResultSolidity();
+ Map fullMap = jsonRpc.getEventFilter2ResultFull();
+ String solidityPrefix = "parallel-solid-";
+ for (int i = 0; i < count; i++) {
+ solidityMap.put(solidityPrefix + i, new LogFilterAndResult(fr, 0L, null));
+ }
+ LogFilterAndResult fullFilter = new LogFilterAndResult(fr, 0L, null);
+ fullMap.put("parallel-solid-full-0", fullFilter);
+
+ LogsFilterCapsule capsule =
+ new LogsFilterCapsule(150L, "0xabcdef", null, txInfoList, true, false);
+ jsonRpc.handleLogsFilter(capsule);
+
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals("solidity filter " + i + " must receive one event",
+ 1, solidityMap.get(solidityPrefix + i).getResult().size());
+ }
+ Assert.assertTrue("full-map filter must not receive events",
+ fullFilter.getResult().isEmpty());
+ }
+}
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 65defdab2ed..2ab455fa580 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java
@@ -211,7 +211,9 @@ private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, Estima
});
}
- return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager);
+ TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet);
+ rpc.setManager(mockManager);
+ return rpc;
}
private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults)
@@ -237,7 +239,9 @@ private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constant
.build();
});
- return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager);
+ TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet);
+ rpc.setManager(mockManager);
+ return rpc;
}
private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long energyValue,
@@ -272,6 +276,8 @@ private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long en
});
}
- return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager);
+ TronJsonRpcImpl rpc = new TronJsonRpcImpl(mockNodeInfo, mockWallet);
+ rpc.setManager(mockManager);
+ return rpc;
}
}
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 bd357101da3..5f577194dff 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java
@@ -8,12 +8,14 @@
import java.util.ArrayList;
import java.util.BitSet;
+import java.util.Collections;
import java.util.List;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Assert;
import org.junit.Test;
import org.tron.common.bloom.Bloom;
import org.tron.common.crypto.Hash;
+import org.tron.common.parameter.CommonParameter;
import org.tron.common.runtime.vm.DataWord;
import org.tron.common.utils.ByteArray;
import org.tron.common.utils.ByteUtil;
@@ -242,6 +244,58 @@ public void testLogFilter() {
}
}
+ @Test
+ public void testLogFilterAddressSizeLimit() {
+ // Two valid 20-byte addresses (40 hex chars with 0x prefix)
+ String addr1 = "0xaa6612f03443517ced2bdcf27958c22353ceeab9";
+ String addr2 = "0xbb7723a04554628ced3cdf38069b433464ffbc0a";
+ String addr3 = "0xcc8834b15665739def4de049f17a544575aabcd1";
+
+ int savedLimit = CommonParameter.getInstance().jsonRpcMaxAddressSize;
+ try {
+ CommonParameter.getInstance().jsonRpcMaxAddressSize = 2;
+
+ // Exactly at limit — must not throw
+ ArrayList atLimit = new ArrayList<>();
+ atLimit.add(addr1);
+ atLimit.add(addr2);
+ FilterRequest frAtLimit = new FilterRequest();
+ frAtLimit.setAddress(atLimit);
+ try {
+ new LogFilter(frAtLimit);
+ } catch (JsonRpcInvalidParamsException e) {
+ Assert.fail("address list at limit should not throw: " + e.getMessage());
+ }
+
+ // One over limit — must throw with expected message
+ ArrayList overLimit = new ArrayList<>();
+ overLimit.add(addr1);
+ overLimit.add(addr2);
+ overLimit.add(addr3);
+ FilterRequest frOverLimit = new FilterRequest();
+ frOverLimit.setAddress(overLimit);
+ try {
+ new LogFilter(frOverLimit);
+ Assert.fail("address list over limit should have thrown JsonRpcInvalidParamsException");
+ } catch (JsonRpcInvalidParamsException e) {
+ Assert.assertTrue(e.getMessage().contains("exceed max addresses:"));
+ }
+
+ // Limit = 0 means disabled — large list must pass
+ CommonParameter.getInstance().jsonRpcMaxAddressSize = 0;
+ ArrayList largeList = new ArrayList<>(Collections.nCopies(500, addr1));
+ FilterRequest frDisabled = new FilterRequest();
+ frDisabled.setAddress(largeList);
+ try {
+ new LogFilter(frDisabled);
+ } catch (JsonRpcInvalidParamsException e) {
+ Assert.fail("limit=0 should disable the check: " + e.getMessage());
+ }
+ } finally {
+ CommonParameter.getInstance().jsonRpcMaxAddressSize = savedLimit;
+ }
+ }
+
private int[] getBloomIndex(String s) {
Bloom bloom = Bloom.create(Hash.sha3(ByteArray.fromHexString(s)));
BitSet bs = BitSet.valueOf(bloom.getData());
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 9a1641f9e45..f753045d259 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java
@@ -214,7 +214,8 @@ public void init() {
dbManager.getTransactionRetStore()
.put(ByteArray.fromLong(blockCapsule2.getNum()), transactionRetCapsule2);
- tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, dbManager);
+ tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
}
@Test
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java
new file mode 100644
index 00000000000..77f869fd5a8
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchOverLimitTest.java
@@ -0,0 +1,151 @@
+package org.tron.core.jsonrpc;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.protobuf.ByteString;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.tron.api.GrpcAPI.TransactionInfoList;
+import org.tron.common.utils.Sha256Hash;
+import org.tron.core.ChainBaseManager;
+import org.tron.core.capsule.BlockCapsule;
+import org.tron.core.db.Manager;
+import org.tron.core.exception.BadItemException;
+import org.tron.core.exception.ItemNotFoundException;
+import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException;
+import org.tron.core.exception.jsonrpc.JsonRpcTooManyResultException;
+import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
+import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement;
+import org.tron.core.services.jsonrpc.filters.LogBlockQuery;
+import org.tron.core.services.jsonrpc.filters.LogFilterWrapper;
+import org.tron.core.services.jsonrpc.filters.LogMatch;
+import org.tron.protos.Protocol.TransactionInfo;
+import org.tron.protos.Protocol.TransactionInfo.Log;
+
+/**
+ * Verifies the over-limit check in {@link LogMatch#matchBlockOneByOne()}
+ * The fix ensures the exception is thrown BEFORE {@code addAll}, so the result list never
+ * silently exceeds {@link LogBlockQuery#MAX_RESULT}.
+ */
+public class LogMatchOverLimitTest {
+
+ private static final int MAX_RESULT = LogBlockQuery.MAX_RESULT; // 10000
+
+ /** Builds a TransactionInfoList containing one TransactionInfo with {@code logCount} logs. */
+ private TransactionInfoList buildTxList(int logCount) {
+ TransactionInfo.Builder txBuilder = TransactionInfo.newBuilder();
+ for (int i = 0; i < logCount; i++) {
+ txBuilder.addLog(Log.newBuilder()
+ .setAddress(ByteString.copyFrom(new byte[20]))
+ .build());
+ }
+ return TransactionInfoList.newBuilder()
+ .addTransactionInfo(txBuilder.build())
+ .build();
+ }
+
+ private Manager buildMockManager(long blockNum, TransactionInfoList txList)
+ throws ItemNotFoundException {
+ Manager manager = mock(Manager.class);
+ ChainBaseManager chainBaseManager = mock(ChainBaseManager.class);
+ BlockCapsule.BlockId blockId = new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, blockNum);
+
+ when(manager.getChainBaseManager()).thenReturn(chainBaseManager);
+ when(chainBaseManager.getBlockIdByNum(anyLong())).thenReturn(blockId);
+ when(manager.getTransactionInfoByBlockNum(blockNum)).thenReturn(txList);
+ return manager;
+ }
+
+ private Manager buildMockManager(long block1, TransactionInfoList txList1,
+ long block2, TransactionInfoList txList2) throws ItemNotFoundException {
+ Manager manager = mock(Manager.class);
+ ChainBaseManager chainBaseManager = mock(ChainBaseManager.class);
+ BlockCapsule.BlockId blockId = new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 0);
+
+ when(manager.getChainBaseManager()).thenReturn(chainBaseManager);
+ when(chainBaseManager.getBlockIdByNum(anyLong())).thenReturn(blockId);
+ when(manager.getTransactionInfoByBlockNum(block1)).thenReturn(txList1);
+ when(manager.getTransactionInfoByBlockNum(block2)).thenReturn(txList2);
+ return manager;
+ }
+
+ private LogMatch buildLogMatch(List blockNums, Manager manager)
+ throws JsonRpcInvalidParamsException {
+ FilterRequest fr = new FilterRequest(); // match-all filter
+ LogFilterWrapper wrapper = new LogFilterWrapper(fr, 0L, null, false);
+ return new LogMatch(wrapper, blockNums, manager);
+ }
+
+ /** Under the limit: all logs returned without exception. */
+ @Test
+ public void testUnderLimit_returnsAllResults()
+ throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
+ JsonRpcInvalidParamsException {
+ int logCount = MAX_RESULT / 2; // 5000, well under limit
+ Manager manager = buildMockManager(100L, buildTxList(logCount));
+ LogMatch logMatch = buildLogMatch(Collections.singletonList(100L), manager);
+
+ LogFilterElement[] results = logMatch.matchBlockOneByOne();
+ Assert.assertEquals(logCount, results.length);
+ }
+
+ /**
+ * The cumulative log count from two blocks equals exactly MAX_RESULT.
+ * This should succeed (boundary: equal is still OK).
+ */
+ @Test
+ public void testAtExactLimit_succeeds()
+ throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
+ JsonRpcInvalidParamsException {
+ // block 1: MAX_RESULT - 1 logs, block 2: 1 log → total == MAX_RESULT
+ Manager manager = buildMockManager(
+ 1L, buildTxList(MAX_RESULT - 1),
+ 2L, buildTxList(1));
+ LogMatch logMatch = buildLogMatch(Arrays.asList(1L, 2L), manager);
+
+ LogFilterElement[] results = logMatch.matchBlockOneByOne();
+ Assert.assertEquals(MAX_RESULT, results.length);
+ }
+
+ /**
+ * Verifies the fix: when the second block would push the total over MAX_RESULT,
+ * {@link JsonRpcTooManyResultException} is thrown BEFORE {@code addAll}.
+ */
+ @Test
+ public void testExceedsLimit_throws()
+ throws ItemNotFoundException, JsonRpcInvalidParamsException {
+ // block 1: MAX_RESULT - 1 logs, block 2: 2 logs → 9999 + 2 = 10001 > MAX_RESULT
+ Manager manager = buildMockManager(
+ 1L, buildTxList(MAX_RESULT - 1),
+ 2L, buildTxList(2));
+ LogMatch logMatch = buildLogMatch(Arrays.asList(1L, 2L), manager);
+
+ assertThrows(JsonRpcTooManyResultException.class, logMatch::matchBlockOneByOne);
+ }
+
+ /** A block with no matching logs is skipped without incrementing the result count. */
+ @Test
+ public void testEmptyBlockSkipped()
+ throws BadItemException, ItemNotFoundException, JsonRpcTooManyResultException,
+ JsonRpcInvalidParamsException {
+ // block 1: no logs (empty txInfoList → skipped), block 2: 3 logs
+ Manager manager = mock(Manager.class);
+ ChainBaseManager chainBaseManager = mock(ChainBaseManager.class);
+ BlockCapsule.BlockId blockId = new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 0);
+ when(manager.getChainBaseManager()).thenReturn(chainBaseManager);
+ when(chainBaseManager.getBlockIdByNum(anyLong())).thenReturn(blockId);
+ when(manager.getTransactionInfoByBlockNum(1L))
+ .thenReturn(TransactionInfoList.newBuilder().build()); // empty
+ when(manager.getTransactionInfoByBlockNum(2L)).thenReturn(buildTxList(3));
+
+ LogMatch logMatch = buildLogMatch(Arrays.asList(1L, 2L), manager);
+ LogFilterElement[] results = logMatch.matchBlockOneByOne();
+ Assert.assertEquals(3, results.length);
+ }
+}
diff --git a/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java b/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java
index fdd9cb44222..24ca71a74bc 100644
--- a/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java
+++ b/framework/src/test/java/org/tron/core/jsonrpc/WalletCursorTest.java
@@ -1,6 +1,9 @@
package org.tron.core.jsonrpc;
import com.google.protobuf.ByteString;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
@@ -13,14 +16,18 @@
import org.tron.core.capsule.AccountCapsule;
import org.tron.core.config.args.Args;
import org.tron.core.db2.core.Chainbase.Cursor;
+import org.tron.core.exception.jsonrpc.JsonRpcExceedLimitException;
import org.tron.core.services.NodeInfoService;
+import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest;
import org.tron.core.services.jsonrpc.TronJsonRpcImpl;
import org.tron.core.services.jsonrpc.TronJsonRpcImpl.RequestSource;
+import org.tron.core.services.jsonrpc.filters.LogFilterAndResult;
import org.tron.core.services.jsonrpc.types.BuildArguments;
import org.tron.protos.Protocol;
@Slf4j
public class WalletCursorTest extends BaseTest {
+
private static final String OWNER_ADDRESS;
private static final String OWNER_ADDRESS_ACCOUNT_NAME = "first";
@Resource
@@ -30,7 +37,7 @@ public class WalletCursorTest extends BaseTest {
private static boolean init;
static {
- Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF);
+ Args.setParam(new String[] {"--output-directory", dbPath()}, TestConstants.TEST_CONF);
OWNER_ADDRESS =
Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc";
@@ -53,7 +60,8 @@ public void init() {
@Test
public void testSource() {
- TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, dbManager);
+ TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
Assert.assertEquals(Cursor.HEAD, wallet.getCursor());
Assert.assertEquals(RequestSource.FULLNODE, tronJsonRpc.getSource());
@@ -84,9 +92,11 @@ public void testDisableInSolidity() {
dbManager.setCursor(Cursor.SOLIDITY);
- TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, dbManager);
+ TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
try {
tronJsonRpc.buildTransaction(buildArguments);
+ tronJsonRpc.close();
} catch (Exception e) {
Assert.assertEquals("the method buildTransaction does not exist/is not available in "
+ "SOLIDITY", e.getMessage());
@@ -105,7 +115,8 @@ public void testDisableInPBFT() {
dbManager.setCursor(Cursor.PBFT);
- TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, dbManager);
+ TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
try {
tronJsonRpc.buildTransaction(buildArguments);
} catch (Exception e) {
@@ -132,13 +143,50 @@ public void testEnableInFullNode() {
buildArguments.setTo("0x548794500882809695a8a687866e76d4271a1abc");
buildArguments.setValue("0x1f4");
- TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet, dbManager);
+ TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
try {
tronJsonRpc.buildTransaction(buildArguments);
+ tronJsonRpc.close();
} catch (Exception e) {
Assert.fail();
}
}
+ /**
+ * When the active filter count reaches the configured cap (node.jsonrpc.maxLogFilterNum),
+ * eth_newFilter must throw JsonRpcExceedLimitException instead of growing without bound.
+ */
+ @Test
+ public void testNewFilter_exceedsCapThrowsException() throws Exception {
+ int cap = 5;
+ int saved = Args.getInstance().getJsonRpcMaxLogFilterNum();
+ Args.getInstance().setJsonRpcMaxLogFilterNum(cap);
+ FilterRequest fr = new FilterRequest();
+ TronJsonRpcImpl tronJsonRpc = new TronJsonRpcImpl(nodeInfoService, wallet);
+ tronJsonRpc.setManager(dbManager);
+ Map map = tronJsonRpc.getEventFilter2ResultFull();
+ List addedKeys = new ArrayList<>();
+
+ try {
+ for (int i = 0; i < cap; i++) {
+ String key = "walletcursor-cap-test-" + i;
+ map.put(key, new LogFilterAndResult(fr, 0L, null));
+ addedKeys.add(key);
+ }
+ Assert.assertEquals(cap, addedKeys.size());
+
+ try {
+ tronJsonRpc.newFilter(fr);
+ Assert.fail("Expected JsonRpcExceedLimitException when filter count reaches cap");
+ } catch (JsonRpcExceedLimitException e) {
+ Assert.assertTrue(e.getMessage().contains(String.valueOf(cap)));
+ }
+ } finally {
+ tronJsonRpc.close();
+ Args.getInstance().setJsonRpcMaxLogFilterNum(saved);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java
index 3083d36425a..7e584581d2b 100644
--- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java
+++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java
@@ -16,8 +16,10 @@
import org.tron.core.capsule.BlockCapsule;
import org.tron.core.capsule.TransactionCapsule;
import org.tron.core.config.args.Args;
+import org.tron.core.db.Manager;
import org.tron.core.exception.P2pException;
import org.tron.core.exception.P2pException.TypeEnum;
+import org.tron.core.store.DynamicPropertiesStore;
import org.tron.protos.Protocol.Transaction.Contract.ContractType;
import org.tron.protos.contract.BalanceContract.TransferContract;
@@ -58,6 +60,86 @@ public void test() throws Exception {
Assert.assertTrue(!tronNetDelegate.isBlockUnsolidified());
}
+ // ── pushVerifiedBlock tests ───────────────────────────────────────────────────
+
+ /**
+ * When hitDown is already true, processBlock returns immediately without
+ * calling pushBlock and pushVerifiedBlock must not throw.
+ */
+ @Test
+ public void testPushVerifiedBlockSkipsWhenHitDown() throws Exception {
+ Args.setParam(new String[] {}, TestConstants.TEST_CONF);
+ TronNetDelegate tronNetDelegate = new TronNetDelegate();
+ setField(tronNetDelegate, "hitDown", true);
+
+ BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY);
+ tronNetDelegate.pushVerifiedBlock(block);
+
+ Assert.assertTrue(block.generatedByMyself);
+ Assert.assertTrue(tronNetDelegate.isHitDown());
+ }
+
+ /**
+ * When the conditional-shutdown threshold is reached, processBlock must set
+ * hitDown=true and return without calling pushBlock.
+ */
+ @Test
+ public void testPushVerifiedBlockTriggersShutdown() throws Exception {
+ Args.setParam(new String[] {}, TestConstants.TEST_CONF);
+ TronNetDelegate tronNetDelegate = new TronNetDelegate();
+ tronNetDelegate.init();
+ tronNetDelegate.setExit(false); // prevent System.exit(0) in hit-thread
+
+ Manager dbManager = Mockito.mock(Manager.class);
+ Mockito.when(dbManager.getLatestSolidityNumShutDown()).thenReturn(50L);
+ DynamicPropertiesStore store = Mockito.mock(DynamicPropertiesStore.class);
+ Mockito.when(store.getLatestBlockHeaderNumberFromDB()).thenReturn(50L);
+ Mockito.when(dbManager.getDynamicPropertiesStore()).thenReturn(store);
+ setField(tronNetDelegate, "dbManager", dbManager);
+
+ BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY);
+ try {
+ tronNetDelegate.pushVerifiedBlock(block);
+ } finally {
+ tronNetDelegate.close();
+ }
+
+ Assert.assertTrue(tronNetDelegate.isHitDown());
+ Mockito.verify(dbManager, Mockito.never()).pushBlock(Mockito.any());
+ }
+
+ /**
+ * On the normal (non-shutdown) path pushBlock must be called exactly once.
+ */
+ @Test
+ public void testPushVerifiedBlockPushesBlock() throws Exception {
+ Args.setParam(new String[] {}, TestConstants.TEST_CONF);
+ TronNetDelegate tronNetDelegate = new TronNetDelegate();
+
+ Manager dbManager = Mockito.mock(Manager.class);
+ Mockito.when(dbManager.getLatestSolidityNumShutDown()).thenReturn(0L);
+ Mockito.when(dbManager.getBlockedTimer()).thenReturn(new ThreadLocal<>());
+
+ ChainBaseManager chainBaseManager = Mockito.mock(ChainBaseManager.class);
+ Mockito.when(chainBaseManager.getHeadBlockId())
+ .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 0L));
+
+ setField(tronNetDelegate, "dbManager", dbManager);
+ setField(tronNetDelegate, "chainBaseManager", chainBaseManager);
+
+ BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY);
+ tronNetDelegate.pushVerifiedBlock(block);
+
+ Assert.assertTrue(block.generatedByMyself);
+ Mockito.verify(dbManager, Mockito.times(1)).pushBlock(Mockito.any());
+ }
+
+ private static void setField(Object obj, String name, Object value) throws Exception {
+ Field f = obj.getClass().getDeclaredField(name);
+ f.setAccessible(true);
+ f.set(obj, value);
+ }
+
@Test
public void testValidBlockMerkleRoot() throws Exception {
Args.setParam(new String[] {}, TestConstants.TEST_CONF);
diff --git a/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
new file mode 100644
index 00000000000..d7828fa5cd0
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/BufferedResponseWrapperTest.java
@@ -0,0 +1,287 @@
+package org.tron.core.services.filter;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class BufferedResponseWrapperTest {
+
+ private MockHttpServletResponse mockResp;
+
+ @Before
+ public void setUp() {
+ mockResp = new MockHttpServletResponse();
+ }
+
+ // --- isOverflow: false cases ---
+
+ @Test
+ public void noLimit_neverOverflows() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.getOutputStream().write(new byte[1024 * 1024]);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void withinLimit_notOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10);
+ w.getOutputStream().write(new byte[10]);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void exactlyAtLimit_notOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
+ assertFalse(w.isOverflow());
+ }
+
+ // --- isOverflow: true via write ---
+
+ @Test
+ public void oneBytePastLimit_overflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 5);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5, 6});
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void singleByteWrite_triggerOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 3);
+ w.getOutputStream().write(1);
+ w.getOutputStream().write(2);
+ w.getOutputStream().write(3);
+ assertFalse(w.isOverflow());
+ w.getOutputStream().write(4);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void overflow_bufferIsReleasedOnOverflow() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 4);
+ w.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
+ assertTrue(w.isOverflow());
+ // After overflow, further writes are silently discarded — no exception
+ w.getOutputStream().write(new byte[100]);
+ assertTrue(w.isOverflow());
+ }
+
+ // --- isOverflow: true via setContentLength ---
+
+ @Test
+ public void setContentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLength(101);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLength_exactlyAtLimit_notOverflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLength(100);
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLengthLong_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setContentLengthLong(101L);
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setContentLength_noLimit_neverOverflows() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setContentLength(Integer.MAX_VALUE);
+ assertFalse(w.isOverflow());
+ }
+
+ // --- setContentLength early detection: writes after early overflow are discarded ---
+
+ @Test
+ public void earlyOverflow_subsequentWritesDiscarded() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 10);
+ w.setContentLength(20);
+ assertTrue(w.isOverflow());
+ w.getOutputStream().write(new byte[5]);
+ // Nothing committed to actual response
+ assertFalse(mockResp.isCommitted());
+ }
+
+ // --- commitToResponse ---
+
+ @Test
+ public void commitToResponse_writesBodyAndHeaders() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
+ w.setStatus(200);
+ w.setContentType("application/json");
+ w.getOutputStream().write(data);
+ w.commitToResponse();
+
+ assertEquals(200, mockResp.getStatus());
+ assertEquals("application/json", mockResp.getContentType());
+ assertArrayEquals(data, mockResp.getContentAsByteArray());
+ }
+
+ @Test
+ public void commitToResponse_setsCorrectContentLength() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ byte[] data = new byte[]{10, 20, 30};
+ w.getOutputStream().write(data);
+ w.commitToResponse();
+
+ assertEquals(3, mockResp.getContentLength());
+ }
+
+ @Test
+ public void commitToResponse_emptyBuffer_writesZeroBytes() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setStatus(200);
+ w.commitToResponse();
+
+ assertEquals(0, mockResp.getContentLength());
+ assertEquals(0, mockResp.getContentAsByteArray().length);
+ }
+
+ // --- header buffering: nothing reaches actual response until commit ---
+
+ @Test
+ public void statusNotForwardedBeforeCommit() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setStatus(201);
+ // MockHttpServletResponse defaults to 200
+ assertEquals(200, mockResp.getStatus());
+ w.commitToResponse();
+ assertEquals(201, mockResp.getStatus());
+ }
+
+ // --- getStatus() ---
+
+ @Test
+ public void getStatus_returnsBufferedValue() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setStatus(404);
+ assertEquals(404, w.getStatus());
+ // actual response must still be untouched
+ assertEquals(200, mockResp.getStatus());
+ }
+
+ @Test
+ public void getStatus_defaultIs200() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ assertEquals(200, w.getStatus());
+ }
+
+ // --- setHeader / addHeader for Content-Length ---
+
+ @Test
+ public void setHeader_contentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "101");
+ assertTrue(w.isOverflow());
+ // Content-Length must NOT have been forwarded to the actual response
+ assertNull(mockResp.getHeader("Content-Length"));
+ }
+
+ @Test
+ public void setHeader_contentLength_withinLimit_noOverflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "100");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_contentLength_caseInsensitive_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 50);
+ w.setHeader("content-length", "51");
+ assertTrue(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_contentLength_malformed_ignored() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.setHeader("Content-Length", "not-a-number");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void setHeader_nonContentLength_passesThroughToActual() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.setHeader("X-Custom-Header", "hello");
+ assertEquals("hello", mockResp.getHeader("X-Custom-Header"));
+ }
+
+ @Test
+ public void addHeader_contentLength_exceedsLimit_overflow() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.addHeader("Content-Length", "200");
+ assertTrue(w.isOverflow());
+ assertNull(mockResp.getHeader("Content-Length"));
+ }
+
+ @Test
+ public void addHeader_contentLength_malformed_ignored() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 100);
+ w.addHeader("Content-Length", "bad");
+ assertFalse(w.isOverflow());
+ }
+
+ @Test
+ public void addHeader_nonContentLength_passesThroughToActual() {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.addHeader("X-Trace-Id", "abc123");
+ assertEquals("abc123", mockResp.getHeader("X-Trace-Id"));
+ }
+
+ // --- commitToResponse idempotency ---
+
+ @Test(expected = IllegalStateException.class)
+ public void commitToResponse_secondCall_throwsIllegalState() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.commitToResponse();
+ w.commitToResponse();
+ }
+
+ // --- getWriter path ---
+
+ @Test
+ public void writeViaWriter_commitToResponse_flushesBody() throws IOException {
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.getWriter().print("hello");
+ w.getWriter().flush();
+ w.commitToResponse();
+ assertEquals("hello", mockResp.getContentAsString());
+ }
+
+ @Test
+ public void writeViaWriter_noExplicitFlush_commitToResponse_flushesBody() throws IOException {
+ // Regression: PrintWriter(autoFlush=true) does NOT flush on plain print(); bytes can sit
+ // in the OutputStreamWriter encoder until commitToResponse() flushes the writer internally.
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 0);
+ w.getWriter().print("hello");
+ w.commitToResponse();
+ assertEquals("hello", mockResp.getContentAsString());
+ assertEquals(5, mockResp.getContentLength());
+ }
+
+ @Test
+ public void writeViaWriter_noExplicitFlush_flushTripsOverflow() throws IOException {
+ // Regression: bytes buffered in the encoder may push the total past maxBytes when
+ // commitToResponse() flushes — overflow must be detected and nothing written to actual.
+ BufferedResponseWrapper w = new BufferedResponseWrapper(mockResp, 3);
+ w.getWriter().print("hello"); // 5 bytes, not yet in ByteArrayOutputStream
+ assertFalse("overflow must not trigger before flush", w.isOverflow());
+ w.commitToResponse();
+ assertTrue("flush inside commitToResponse must trip overflow", w.isOverflow());
+ assertEquals(0, mockResp.getContentAsByteArray().length);
+ }
+}
diff --git a/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
new file mode 100644
index 00000000000..813b1a61bea
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/filter/CachedBodyRequestWrapperTest.java
@@ -0,0 +1,109 @@
+package org.tron.core.services.filter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+public class CachedBodyRequestWrapperTest {
+
+ private static final byte[] BODY = "hello world".getBytes(StandardCharsets.UTF_8);
+
+ private static byte[] readFully(javax.servlet.ServletInputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buf = new byte[128];
+ int n;
+ while ((n = in.read(buf)) != -1) {
+ out.write(buf, 0, n);
+ }
+ return out.toByteArray();
+ }
+
+ // --- getInputStream ---
+
+ @Test
+ public void getInputStream_returnsBodyContent() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ byte[] read = readFully(w.getInputStream());
+ assertEquals(new String(BODY, StandardCharsets.UTF_8),
+ new String(read, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void getInputStream_calledTwice_bothSucceed() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getInputStream();
+ // second call of the same accessor is allowed by the servlet spec
+ w.getInputStream();
+ }
+
+ // --- getReader ---
+
+ @Test
+ public void getReader_returnsBodyContent() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ String line = w.getReader().readLine();
+ assertEquals("hello world", line);
+ }
+
+ @Test
+ public void getReader_calledTwice_bothSucceed() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getReader();
+ w.getReader();
+ }
+
+ // --- mutual exclusion ---
+
+ @Test(expected = IllegalStateException.class)
+ public void getReader_afterGetInputStream_throws() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getInputStream();
+ w.getReader();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getInputStream_afterGetReader_throws() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ w.getReader();
+ w.getInputStream();
+ }
+
+ // --- stream contract ---
+
+ @Test
+ public void getInputStream_isFinished_afterFullRead() throws IOException {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ javax.servlet.ServletInputStream in = w.getInputStream();
+ while (in.read() != -1) {
+ // drain
+ }
+ assertTrue(in.isFinished());
+ }
+
+ @Test
+ public void getInputStream_isReady_returnsTrue() {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(), BODY);
+ assertTrue(w.getInputStream().isReady());
+ }
+
+ @Test
+ public void getInputStream_emptyBody_isFinishedImmediately() {
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(new MockHttpServletRequest(),
+ new byte[0]);
+ assertTrue(w.getInputStream().isFinished());
+ }
+
+ @Test
+ public void getReader_usesRequestCharacterEncoding() throws IOException {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.setCharacterEncoding("UTF-8");
+ byte[] utf8Body = "tron".getBytes(StandardCharsets.UTF_8);
+ CachedBodyRequestWrapper w = new CachedBodyRequestWrapper(req, utf8Body);
+ assertEquals("tron", w.getReader().readLine());
+ }
+}
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
new file mode 100644
index 00000000000..fa45ca48876
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java
@@ -0,0 +1,264 @@
+package org.tron.core.services.jsonrpc;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.googlecode.jsonrpc4j.JsonRpcServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.tron.common.parameter.CommonParameter;
+
+public class JsonRpcServletTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private TestableServlet servlet;
+ private JsonRpcServer mockRpcServer;
+ private int savedMaxBatchSize;
+ private int savedMaxResponseSize;
+
+ @Before
+ public void setUp() throws Exception {
+ servlet = new TestableServlet();
+ mockRpcServer = mock(JsonRpcServer.class);
+ Field f = JsonRpcServlet.class.getDeclaredField("rpcServer");
+ f.setAccessible(true);
+ f.set(servlet, mockRpcServer);
+ savedMaxBatchSize = CommonParameter.getInstance().jsonRpcMaxBatchSize;
+ savedMaxResponseSize = CommonParameter.getInstance().jsonRpcMaxResponseSize;
+ }
+
+ @After
+ public void tearDown() {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = savedMaxBatchSize;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = savedMaxResponseSize;
+ }
+
+ // --- parse error paths ---
+
+ @Test
+ public void invalidJson_returnsParseError() throws Exception {
+ MockHttpServletResponse resp = doPost("not {{ valid json");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32700, body.get("error").get("code").asInt());
+ assertEquals("2.0", body.get("jsonrpc").asText());
+ assertTrue(body.get("id").isNull());
+ }
+
+ @Test
+ public void emptyBody_returnsParseError() throws Exception {
+ MockHttpServletResponse resp = doPost("");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertEquals(-32700, body.get("error").get("code").asInt());
+ }
+
+ // --- batch size limit ---
+
+ @Test
+ public void batchExceedsLimit_returnsExceedLimitAsArray() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 2;
+ MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch error response must be a JSON array", body.isArray());
+ assertEquals(1, body.size());
+ assertEquals(-32005, body.get(0).get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchWithinLimit_proceedsToRpcServer() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 5;
+ byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}"
+ .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("[{\"id\":1},{\"id\":2}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsByteArray());
+ assertTrue("batch response must be a JSON array", body.isArray());
+ assertEquals("each sub-request must produce a response", 2, body.size());
+ assertEquals("ok", body.get(0).get("result").asText());
+ }
+
+ @Test
+ public void emptyBatch_returnsInvalidRequest() throws Exception {
+ MockHttpServletResponse resp = doPost("[]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse("empty-batch error response must be a single object, not an array", body.isArray());
+ assertEquals(-32600, body.get("error").get("code").asInt());
+ assertEquals("2.0", body.get("jsonrpc").asText());
+ assertTrue(body.get("id").isNull());
+ }
+
+ @Test
+ public void batchLimitDisabled_largeBatchAllowed() throws Exception {
+ CommonParameter.getInstance().jsonRpcMaxBatchSize = 0;
+ // write nothing — simulates notifications (no response expected)
+ doAnswer(inv -> 0).when(mockRpcServer)
+ .handleRequest(any(InputStream.class), any(OutputStream.class));
+
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < 500; i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ sb.append("{}");
+ }
+ sb.append("]");
+ MockHttpServletResponse resp = doPost(sb.toString());
+ assertEquals(200, resp.getStatus());
+ assertEquals("all-notification batch must return empty body per JSON-RPC 2.0 §6",
+ 0, resp.getContentLength());
+ assertEquals("", resp.getContentAsString());
+ }
+
+ // --- rpcServer.handle exceptions ---
+
+ @Test
+ public void rpcServerThrowsRuntimeException_returnsInternalError() throws Exception {
+ doThrow(new RuntimeException("server exploded")).when(mockRpcServer)
+ .handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":42}");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32603, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchRpcServerThrows_internalErrorIsArray() throws Exception {
+ doThrow(new RuntimeException("boom")).when(mockRpcServer)
+ .handleRequest(any(InputStream.class), any(OutputStream.class));
+ MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch internal error must be an array", body.isArray());
+ assertEquals(-32603, body.get(0).get("error").get("code").asInt());
+ }
+
+ // --- response size limit ---
+
+ @Test
+ public void responseTooLarge_returnsSingleErrorObject() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(new byte[limit + 1]);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_getLogs\",\"id\":1}");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertFalse(body.isArray());
+ assertEquals(-32003, body.get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchResponseTooLarge_returnsErrorArray() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ doAnswer(inv -> {
+ OutputStream out = inv.getArgument(1);
+ out.write(new byte[limit + 1]);
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
+
+ MockHttpServletResponse resp = doPost("[{\"method\":\"eth_getLogs\"}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("batch response-too-large must be an array", body.isArray());
+ assertEquals(-32003, body.get(0).get("error").get("code").asInt());
+ }
+
+ @Test
+ public void batchShortCircuitsOnOverflow() throws Exception {
+ int limit = 50;
+ CommonParameter.getInstance().jsonRpcMaxResponseSize = limit;
+ int[] callCount = {0};
+ doAnswer(inv -> {
+ OutputStream out = inv.getArgument(1);
+ callCount[0]++;
+ if (callCount[0] == 1) {
+ out.write("{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8));
+ } else {
+ out.write(new byte[limit]); // triggers overflow when added to accumulated size
+ }
+ return 0;
+ }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class));
+
+ MockHttpServletResponse resp = doPost("[{\"id\":1},{\"id\":2},{\"id\":3}]");
+ assertEquals(200, resp.getStatus());
+ JsonNode body = MAPPER.readTree(resp.getContentAsString());
+ assertTrue("overflow response must be an array", body.isArray());
+ // Geth-compatible: previous successes are preserved; overflow item and remaining
+ // unexecuted items each get a -32003 error with their original id.
+ assertEquals(3, body.size());
+ assertEquals("ok", body.get(0).get("result").asText());
+ assertEquals(-32003, body.get(1).get("error").get("code").asInt());
+ assertEquals(2, body.get(1).get("id").asInt());
+ assertEquals(-32003, body.get(2).get("error").get("code").asInt());
+ assertEquals(3, body.get(2).get("id").asInt());
+ assertEquals("third sub-request must not be executed after overflow", 2, callCount[0]);
+ }
+
+ // --- normal path ---
+
+ @Test
+ public void normalRequest_commitsRpcServerResponse() throws Exception {
+ byte[] rpcResp = "{\"result\":\"0x1\"}".getBytes(StandardCharsets.UTF_8);
+ doAnswer(inv -> {
+ HttpServletResponse r = inv.getArgument(1);
+ r.getOutputStream().write(rpcResp);
+ return null;
+ }).when(mockRpcServer).handle(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ MockHttpServletResponse resp = doPost("{\"method\":\"eth_blockNumber\",\"id\":1}");
+ assertEquals(200, resp.getStatus());
+ assertArrayEquals(rpcResp, resp.getContentAsByteArray());
+ }
+
+ // --- helpers ---
+
+ private MockHttpServletResponse doPost(String body) throws Exception {
+ MockHttpServletRequest req = new MockHttpServletRequest("POST", "/jsonrpc");
+ req.setContent(body.getBytes(StandardCharsets.UTF_8));
+ MockHttpServletResponse resp = new MockHttpServletResponse();
+ servlet.callDoPost(req, resp);
+ return resp;
+ }
+
+ private static class TestableServlet extends JsonRpcServlet {
+
+ void callDoPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ doPost(req, resp);
+ }
+ }
+}
diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java
index f5b525cd445..a02eb22364e 100755
--- a/framework/src/test/java/org/tron/program/SolidityNodeTest.java
+++ b/framework/src/test/java/org/tron/program/SolidityNodeTest.java
@@ -1,34 +1,41 @@
package org.tron.program;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.inOrder;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import com.google.protobuf.ByteString;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
-import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.core.type.AnnotatedTypeMetadata;
import org.tron.common.BaseTest;
import org.tron.common.TestConstants;
-import org.tron.common.application.Application;
import org.tron.common.client.DatabaseGrpcClient;
-import org.tron.common.es.ExecutorServiceManager;
+import org.tron.common.utils.ByteArray;
import org.tron.common.utils.PublicMethod;
+import org.tron.core.ChainBaseManager;
import org.tron.core.config.args.Args;
-import org.tron.core.exception.TronError;
+import org.tron.core.net.TronNetDelegate;
import org.tron.core.services.RpcApiService;
import org.tron.core.services.http.solidity.SolidityNodeHttpApiService;
+import org.tron.core.store.DynamicPropertiesStore;
+import org.tron.protos.Protocol;
import org.tron.protos.Protocol.Block;
import org.tron.protos.Protocol.DynamicProperties;
@@ -39,6 +46,9 @@ public class SolidityNodeTest extends BaseTest {
RpcApiService rpcApiService;
@Resource
SolidityNodeHttpApiService solidityNodeHttpApiService;
+ @Resource
+ SolidityNode solidityNode;
+
static int rpcPort = PublicMethod.chooseRandomPort();
static int solidityHttpPort = PublicMethod.chooseRandomPort();
@@ -51,18 +61,22 @@ public class SolidityNodeTest extends BaseTest {
Args.getInstance().setSolidityHttpPort(solidityHttpPort);
}
- @Test
- public void testSolidityArgs() {
- Assert.assertNotNull(Args.getInstance().getTrustNodeAddr());
- Assert.assertTrue(Args.getInstance().isSolidityNode());
- String trustNodeAddr = Args.getInstance().getTrustNodeAddr();
- Args.getInstance().setTrustNodeAddr(null);
- TronError thrown = assertThrows(TronError.class,
- SolidityNode::start);
- assertEquals(TronError.ErrCode.SOLID_NODE_INIT, thrown.getErrCode());
- Args.getInstance().setTrustNodeAddr(trustNodeAddr);
+ // ── helpers ──────────────────────────────────────────────────────────────────
+
+ private boolean getFlag() throws Exception {
+ Field f = SolidityNode.class.getDeclaredField("flag");
+ f.setAccessible(true);
+ return (boolean) f.get(solidityNode);
}
+ private void setFlag(boolean value) throws Exception {
+ Field f = SolidityNode.class.getDeclaredField("flag");
+ f.setAccessible(true);
+ f.set(solidityNode, value);
+ }
+
+ // ── existing tests ────────────────────────────────────────────────────────────
+
@Test
public void testSolidityGrpcCall() {
rpcApiService.start();
@@ -101,93 +115,541 @@ public void testSolidityNodeHttpApiService() {
Assert.assertTrue(true);
}
- @Test
- public void testAwaitShutdownAlwaysStopsNode() {
- Application app = mock(Application.class);
- SolidityNode node = mock(SolidityNode.class);
+ // ── new tests ─────────────────────────────────────────────────────────────────
- SolidityNode.awaitShutdown(app, node);
+ /**
+ * @PostConstruct init() must create both executor services before run() is called.
+ */
+ @Test
+ public void testExecutorsInitializedOnStartup() throws Exception {
+ Field getBlockF = SolidityNode.class.getDeclaredField("getBlockExecutor");
+ getBlockF.setAccessible(true);
+ Field processBlockF = SolidityNode.class.getDeclaredField("processBlockExecutor");
+ processBlockF.setAccessible(true);
- InOrder inOrder = inOrder(app, node);
- inOrder.verify(app).blockUntilShutdown();
- inOrder.verify(node).shutdown();
+ assertNotNull(getBlockF.get(solidityNode));
+ assertNotNull(processBlockF.get(solidityNode));
+ assertFalse(((ExecutorService) getBlockF.get(solidityNode)).isShutdown());
+ assertFalse(((ExecutorService) processBlockF.get(solidityNode)).isShutdown());
}
+ /**
+ * onApplicationEvent() must set flag=false so threads stop before
+ * other beans' @PreDestroy methods are called.
+ */
@Test
- public void testAwaitShutdownStopsNodeWhenBlockedCallFails() {
- Application app = mock(Application.class);
- SolidityNode node = mock(SolidityNode.class);
- RuntimeException expected = new RuntimeException("boom");
- doThrow(expected).when(app).blockUntilShutdown();
+ public void testOnApplicationEventSetsFlagFalse() throws Exception {
+ assertTrue(getFlag());
+ solidityNode.onApplicationEvent(mock(ContextClosedEvent.class));
+ assertFalse(getFlag());
+ setFlag(true); // restore shared bean
+ }
- RuntimeException thrown = assertThrows(RuntimeException.class,
- () -> SolidityNode.awaitShutdown(app, node));
- assertSame(expected, thrown);
+ /**
+ * getBlockByNum() must throw RuntimeException (not return null) when
+ * flag=false, to prevent NullPointerException in blockQueue.put().
+ */
+ @Test(timeout = 1000)
+ public void testGetBlockByNumThrowsWhenClosed() throws Exception {
+ setFlag(false);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class);
+ m.setAccessible(true);
+ try {
+ m.invoke(solidityNode, 1L);
+ Assert.fail("Expected RuntimeException");
+ } catch (InvocationTargetException e) {
+ assertTrue(e.getCause() instanceof RuntimeException);
+ assertEquals("SolidityNode is closing.", e.getCause().getMessage());
+ }
+ } finally {
+ setFlag(true);
+ }
+ }
- InOrder inOrder = inOrder(app, node);
- inOrder.verify(app).blockUntilShutdown();
- inOrder.verify(node).shutdown();
+ /**
+ * getLastSolidityBlockNum() must return 0 (not throw) when flag=false so
+ * getBlock()'s while(flag) loop exits quietly without a misleading error log.
+ */
+ @Test(timeout = 1000)
+ public void testGetLastSolidityBlockNumReturnsZeroWhenClosed() throws Exception {
+ setFlag(false);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum");
+ m.setAccessible(true);
+ long result = (long) m.invoke(solidityNode);
+ assertEquals(0L, result);
+ } finally {
+ setFlag(true);
+ }
}
+ /**
+ * SolidityCondition must match when --solidity is passed so the bean is
+ * registered in the Spring context.
+ */
@Test
- public void testShutdownSetsFlagAndShutsDownExecutors() throws Exception {
- SolidityNode node = mock(SolidityNode.class);
- doCallRealMethod().when(node).shutdown();
+ public void testSolidityConditionMatchesWhenSolidityFlagSet() {
+ assertTrue(Args.getInstance().isSolidityNode());
+ SolidityNode.SolidityCondition condition = new SolidityNode.SolidityCondition();
+ assertTrue(condition.matches(
+ mock(ConditionContext.class),
+ mock(AnnotatedTypeMetadata.class)));
+ }
- ExecutorService es1 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-get");
- ExecutorService es2 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-process");
+ // ── additional coverage tests ─────────────────────────────────────────────────
- Field flagField = SolidityNode.class.getDeclaredField("flag");
- flagField.setAccessible(true);
- flagField.set(node, true);
+ /**
+ * sleep() must return normally without throwing.
+ */
+ @Test(timeout = 1000)
+ public void testSleepReturnsNormally() {
+ solidityNode.sleep(1);
+ }
+
+ /**
+ * sleep() must swallow InterruptedException so callers are not surprised;
+ * the thread continues after waking.
+ */
+ @Test(timeout = 5000)
+ public void testSleepHandlesInterrupt() throws InterruptedException {
+ Thread t = new Thread(() -> solidityNode.sleep(10_000));
+ t.start();
+ Thread.sleep(50);
+ t.interrupt();
+ t.join(2000);
+ assertFalse("sleep() should have returned after interrupt", t.isAlive());
+ }
- Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs");
- getBlockEsField.setAccessible(true);
- getBlockEsField.set(node, es1);
+ /**
+ * getBlockByNum() must return the block when the gRPC client returns a block
+ * whose number matches the requested number.
+ */
+ @Test(timeout = 2000)
+ public void testGetBlockByNumReturnsMatchingBlock() throws Exception {
+ Block expected = blockWithNum(7L);
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getBlock(7L)).thenReturn(expected);
- Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs");
- processBlockEsField.setAccessible(true);
- processBlockEsField.set(node, es2);
+ Field clientField = getField("databaseGrpcClient");
+ Object orig = clientField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class);
+ m.setAccessible(true);
+ Block result = (Block) m.invoke(solidityNode, 7L);
+ assertEquals(7L, result.getBlockHeader().getRawData().getNumber());
+ } finally {
+ clientField.set(solidityNode, orig);
+ }
+ }
- node.shutdown();
+ /**
+ * getLastSolidityBlockNum() must return the value obtained from the gRPC
+ * client when the call succeeds.
+ */
+ @Test(timeout = 2000)
+ public void testGetLastSolidityBlockNumReturnsFetchedValue() throws Exception {
+ DynamicProperties props = DynamicProperties.newBuilder()
+ .setLastSolidityBlockNum(99L).build();
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getDynamicProperties()).thenReturn(props);
- Assert.assertFalse((boolean) flagField.get(node));
- Assert.assertTrue(es1.isShutdown());
- Assert.assertTrue(es2.isShutdown());
+ Field clientField = getField("databaseGrpcClient");
+ Object orig = clientField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum");
+ m.setAccessible(true);
+ long result = (long) m.invoke(solidityNode);
+ assertEquals(99L, result);
+ } finally {
+ clientField.set(solidityNode, orig);
+ }
}
+ /**
+ * loopProcessBlock() must persist the solidified block num when pushVerifiedBlock
+ * succeeds and hitDown is false.
+ */
+ @Test(timeout = 5000)
+ public void testLoopProcessBlockSavesBlockNumWhenNotHitDown() throws Exception {
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(false);
+
+ long origSolidified = chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum();
+ Field delegateField = getField("tronNetDelegate");
+ Object origDelegate = delegateField.get(solidityNode);
+ delegateField.set(solidityNode, mockDelegate);
+ try {
+ invokeLoopProcessBlock(blockWithNum(55L));
+ assertEquals(55L, chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum());
+ } finally {
+ chainBaseManager.getDynamicPropertiesStore()
+ .saveLatestSolidifiedBlockNum(origSolidified);
+ delegateField.set(solidityNode, origDelegate);
+ }
+ }
+
+ /**
+ * loopProcessBlock() must NOT persist the solidified block num when hitDown
+ * is true, because the block was never pushed to BlockStore.
+ */
+ @Test(timeout = 2000)
+ public void testLoopProcessBlockSkipsSaveWhenHitDown() throws Exception {
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(true);
+
+ long origSolidified = chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum();
+ Field delegateField = getField("tronNetDelegate");
+ Object origDelegate = delegateField.get(solidityNode);
+ delegateField.set(solidityNode, mockDelegate);
+ try {
+ invokeLoopProcessBlock(blockWithNum(56L));
+ assertEquals(origSolidified, chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum());
+ } finally {
+ delegateField.set(solidityNode, origDelegate);
+ }
+ }
+
+ /**
+ * resolveCompatibilityIssueIfUsingFullNodeDatabase() must update the solidified
+ * block num to match headBlockNum when solidity lags behind.
+ */
+ @Test(timeout = 2000)
+ public void testResolveCompatibilityIssueWhenSolidityLagsHead() throws Exception {
+ DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class);
+ Mockito.when(mockStore.getLatestSolidifiedBlockNum()).thenReturn(3L);
+ ChainBaseManager mockCbm = mock(ChainBaseManager.class);
+ Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore);
+ Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(10L);
+
+ Field cbmField = getField("chainBaseManager");
+ Object orig = cbmField.get(solidityNode);
+ cbmField.set(solidityNode, mockCbm);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod(
+ "resolveCompatibilityIssueIfUsingFullNodeDatabase");
+ m.setAccessible(true);
+ m.invoke(solidityNode);
+ } finally {
+ cbmField.set(solidityNode, orig);
+ }
+ Mockito.verify(mockStore).saveLatestSolidifiedBlockNum(10L);
+ }
+
+ // ── shutdown / databaseGrpcClient lifecycle ──────────────────────────────────
+
+ /**
+ * When databaseGrpcClient is non-null at shutdown time, its shutdown() must
+ * be called to close the gRPC channel.
+ */
@Test
- public void testRunInitializesNamedExecutors() throws Exception {
- rpcApiService.start();
- String originalAddr = Args.getInstance().getTrustNodeAddr();
- Args.getInstance().setTrustNodeAddr("127.0.0.1:" + rpcPort);
+ public void testShutdownCallsDatabaseClientShutdown() throws Exception {
+ // Use a standalone instance so we don't destroy the shared Spring executor services.
+ SolidityNode node = new SolidityNode();
+
+ DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class);
+ ChainBaseManager mockCbm = mock(ChainBaseManager.class);
+ Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore);
+ Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(0L);
+ getField("chainBaseManager").set(node, mockCbm);
+
+ Method initM = SolidityNode.class.getDeclaredMethod("init");
+ initM.setAccessible(true);
+ initM.invoke(node);
+
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ getField("databaseGrpcClient").set(node, mockClient);
+
+ Method shutdownM = SolidityNode.class.getDeclaredMethod("shutdown");
+ shutdownM.setAccessible(true);
+ shutdownM.invoke(node);
+
+ Mockito.verify(mockClient).shutdown();
+ }
+
+ // ── getBlock() ───────────────────────────────────────────────────────────────
+
+ /**
+ * getBlock() must fetch a block via gRPC, place it in blockQueue, then exit
+ * when flag becomes false after the first successful fetch.
+ */
+ @Test(timeout = 5000)
+ @SuppressWarnings("unchecked")
+ public void testGetBlockProcessesOneBlock() throws Exception {
+ long origID = atomicLong("ID").get();
+ long origRemote = atomicLong("remoteBlockNum").get();
+
+ atomicLong("ID").set(0L);
+ atomicLong("remoteBlockNum").set(2L); // blockNum=1 <= 2, no sleep needed
+
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getBlock(1L)).thenAnswer(inv -> {
+ setFlag(false); // stop the loop after this iteration
+ return blockWithNum(1L);
+ });
+
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(false);
+
+ Field clientField = getField("databaseGrpcClient");
+ Field delegateField = getField("tronNetDelegate");
+ Object origClient = clientField.get(solidityNode);
+ Object origDelegate = delegateField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ delegateField.set(solidityNode, mockDelegate);
+
+ LinkedBlockingDeque queue =
+ (LinkedBlockingDeque) getField("blockQueue").get(solidityNode);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getBlock");
+ m.setAccessible(true);
+ m.invoke(solidityNode);
+
+ assertEquals(1, queue.size());
+ assertEquals(1L, queue.peek().getBlockHeader().getRawData().getNumber());
+ } finally {
+ setFlag(true);
+ queue.clear();
+ atomicLong("ID").set(origID);
+ atomicLong("remoteBlockNum").set(origRemote);
+ clientField.set(solidityNode, origClient);
+ delegateField.set(solidityNode, origDelegate);
+ }
+ }
+
+ // ── processSolidityBlock() ───────────────────────────────────────────────────
+
+ /**
+ * processSolidityBlock() must drain a block from the queue, process it, and
+ * exit when flag becomes false inside pushVerifiedBlock.
+ */
+ @Test(timeout = 5000)
+ @SuppressWarnings("unchecked")
+ public void testProcessSolidityBlockProcessesQueuedBlock() throws Exception {
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(false);
+ Mockito.doAnswer(inv -> {
+ setFlag(false);
+ return null;
+ }).when(mockDelegate).pushVerifiedBlock(Mockito.any());
+
+ long origSolidified = chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum();
+ Field delegateField = getField("tronNetDelegate");
+ Object origDelegate = delegateField.get(solidityNode);
+ delegateField.set(solidityNode, mockDelegate);
+
+ LinkedBlockingDeque queue =
+ (LinkedBlockingDeque) getField("blockQueue").get(solidityNode);
+ queue.put(blockWithNum(88L));
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("processSolidityBlock");
+ m.setAccessible(true);
+ m.invoke(solidityNode);
+
+ assertEquals(88L, chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum());
+ } finally {
+ setFlag(true);
+ queue.clear();
+ chainBaseManager.getDynamicPropertiesStore()
+ .saveLatestSolidifiedBlockNum(origSolidified);
+ delegateField.set(solidityNode, origDelegate);
+ }
+ }
+
+ /**
+ * processSolidityBlock() must return cleanly when the thread is interrupted
+ * while waiting on blockQueue.poll().
+ */
+ @Test(timeout = 8000)
+ public void testProcessSolidityBlockHandlesInterrupt() throws Exception {
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(false);
+
+ Field delegateField = getField("tronNetDelegate");
+ Object origDelegate = delegateField.get(solidityNode);
+ delegateField.set(solidityNode, mockDelegate);
+
+ Method m = SolidityNode.class.getDeclaredMethod("processSolidityBlock");
+ m.setAccessible(true);
+ Thread t = new Thread(() -> {
+ try {
+ m.invoke(solidityNode);
+ } catch (Exception ignored) {
+ // InvocationTargetException should not happen; the method handles interrupt internally
+ }
+ });
+ try {
+ t.start();
+ Thread.sleep(150); // let the thread enter blockQueue.poll(1000 ms)
+ t.interrupt();
+ t.join(5000);
+ assertFalse("processSolidityBlock must exit after interrupt", t.isAlive());
+ } finally {
+ setFlag(true);
+ delegateField.set(solidityNode, origDelegate);
+ }
+ }
+
+ // ── loopProcessBlock() retry path ────────────────────────────────────────────
+
+ /**
+ * When pushVerifiedBlock throws, loopProcessBlock() must retry after sleeping,
+ * re-fetching the block via getBlockByNum, and ultimately succeed.
+ */
+ @Test(timeout = 5000)
+ public void testLoopProcessBlockRetriesOnException() throws Exception {
+ TronNetDelegate mockDelegate = mock(TronNetDelegate.class);
+ Mockito.when(mockDelegate.isHitDown()).thenReturn(false);
+ Mockito.doThrow(new RuntimeException("push failed"))
+ .doNothing()
+ .when(mockDelegate).pushVerifiedBlock(Mockito.any());
+
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getBlock(33L)).thenReturn(blockWithNum(33L));
+
+ long origSolidified = chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum();
+ Field delegateField = getField("tronNetDelegate");
+ Field clientField = getField("databaseGrpcClient");
+ Object origDelegate = delegateField.get(solidityNode);
+ Object origClient = clientField.get(solidityNode);
+ delegateField.set(solidityNode, mockDelegate);
+ clientField.set(solidityNode, mockClient);
try {
- SolidityNode node = new SolidityNode(dbManager);
+ invokeLoopProcessBlock(blockWithNum(33L));
+ assertEquals(33L, chainBaseManager.getDynamicPropertiesStore()
+ .getLatestSolidifiedBlockNum());
+ } catch (RuntimeException e) {
+ Assert.assertTrue(e.getMessage().contains("push failed"));
+ } finally {
+ chainBaseManager.getDynamicPropertiesStore()
+ .saveLatestSolidifiedBlockNum(origSolidified);
+ delegateField.set(solidityNode, origDelegate);
+ clientField.set(solidityNode, origClient);
+ }
+ }
- Field flagField = SolidityNode.class.getDeclaredField("flag");
- flagField.setAccessible(true);
- flagField.set(node, false);
+ // ── getBlockByNum() retry paths ──────────────────────────────────────────────
- Method runMethod = SolidityNode.class.getDeclaredMethod("run");
- runMethod.setAccessible(true);
- runMethod.invoke(node);
+ /**
+ * When the returned block number does not match, getBlockByNum() must warn
+ * and retry; it must throw RuntimeException when flag becomes false.
+ */
+ @Test(timeout = 5000)
+ public void testGetBlockByNumWarnOnWrongNum() throws Exception {
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getBlock(9L)).thenAnswer(inv -> {
+ setFlag(false); // cause the retry loop to exit
+ return blockWithNum(999L); // deliberately wrong number
+ });
- Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs");
- getBlockEsField.setAccessible(true);
- Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs");
- processBlockEsField.setAccessible(true);
+ Field clientField = getField("databaseGrpcClient");
+ Object orig = clientField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class);
+ m.setAccessible(true);
+ try {
+ m.invoke(solidityNode, 9L);
+ Assert.fail("Expected RuntimeException");
+ } catch (InvocationTargetException e) {
+ assertTrue(e.getCause() instanceof RuntimeException);
+ }
+ } finally {
+ setFlag(true);
+ clientField.set(solidityNode, orig);
+ }
+ }
- ExecutorService getBlockEs = (ExecutorService) getBlockEsField.get(node);
- ExecutorService processBlockEs = (ExecutorService) processBlockEsField.get(node);
+ /**
+ * When the gRPC call throws, getBlockByNum() must log, sleep, and retry;
+ * on the second attempt it must return the correct block.
+ */
+ @Test(timeout = 5000)
+ public void testGetBlockByNumRetriesOnException() throws Exception {
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getBlock(8L))
+ .thenThrow(new RuntimeException("rpc error"))
+ .thenReturn(blockWithNum(8L));
- Assert.assertNotNull(getBlockEs);
- Assert.assertNotNull(processBlockEs);
+ Field clientField = getField("databaseGrpcClient");
+ Object orig = clientField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class);
+ m.setAccessible(true);
+ Block result = (Block) m.invoke(solidityNode, 8L);
+ assertEquals(8L, result.getBlockHeader().getRawData().getNumber());
+ } finally {
+ clientField.set(solidityNode, orig);
+ }
+ }
+
+ // ── getLastSolidityBlockNum() retry path ─────────────────────────────────────
- ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "test-solid-get");
- ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "test-solid-process");
+ /**
+ * When getDynamicProperties() throws, getLastSolidityBlockNum() must log,
+ * sleep, and retry; on the second attempt it must return the fetched value.
+ */
+ @Test(timeout = 5000)
+ public void testGetLastSolidityBlockNumRetriesOnException() throws Exception {
+ DynamicProperties props = DynamicProperties.newBuilder()
+ .setLastSolidityBlockNum(50L).build();
+ DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class);
+ Mockito.when(mockClient.getDynamicProperties())
+ .thenThrow(new RuntimeException("rpc error"))
+ .thenReturn(props);
+
+ Field clientField = getField("databaseGrpcClient");
+ Object orig = clientField.get(solidityNode);
+ clientField.set(solidityNode, mockClient);
+ try {
+ Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum");
+ m.setAccessible(true);
+ long result = (long) m.invoke(solidityNode);
+ assertEquals(50L, result);
} finally {
- Args.getInstance().setTrustNodeAddr(originalAddr);
- rpcApiService.stop();
+ clientField.set(solidityNode, orig);
}
}
+
+ // ── private helpers ──────────────────────────────────────────────────────────
+
+ private static Field getField(String name) throws Exception {
+ Field f = SolidityNode.class.getDeclaredField(name);
+ f.setAccessible(true);
+ return f;
+ }
+
+ private AtomicLong atomicLong(String name) throws Exception {
+ return (AtomicLong) getField(name).get(solidityNode);
+ }
+
+ private static Block blockWithNum(long num) {
+ return Block.newBuilder()
+ .setBlockHeader(
+ Protocol.BlockHeader.newBuilder()
+ .setRawData(
+ Protocol.BlockHeader.raw.newBuilder()
+ .setNumber(num)
+ .setParentHash(ByteString.copyFrom(ByteArray.fromHexString(
+ "0x0000000000000000000000000000000000000000000000000000000000000000")))
+ .build())
+ .build())
+ .build();
+ }
+
+ private void invokeLoopProcessBlock(Block block) throws Exception {
+ Method m = SolidityNode.class.getDeclaredMethod("loopProcessBlock", Block.class);
+ m.setAccessible(true);
+ m.invoke(solidityNode, block);
+ }
}
diff --git a/framework/src/test/resources/config-localtest.conf b/framework/src/test/resources/config-localtest.conf
index 53a78d3e4c6..d31705f39bd 100644
--- a/framework/src/test/resources/config-localtest.conf
+++ b/framework/src/test/resources/config-localtest.conf
@@ -168,6 +168,7 @@ node {
# maxBlockRange = 5000
# maxSubTopics = 1000
# maxBlockFilterNum = 30000
+ # maxLogFilterNum = 20000
}
}
diff --git a/framework/src/test/resources/config-test-mainnet.conf b/framework/src/test/resources/config-test-mainnet.conf
index d39f432ac36..9f968c5628d 100644
--- a/framework/src/test/resources/config-test-mainnet.conf
+++ b/framework/src/test/resources/config-test-mainnet.conf
@@ -95,6 +95,7 @@ node {
# maxBlockRange = 5000
# maxSubTopics = 1000
# maxBlockFilterNum = 50000
+ # maxLogFilterNum = 20000
}
rpc {
diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf
index 71e93f84db5..21cebbfeef4 100644
--- a/framework/src/test/resources/config-test.conf
+++ b/framework/src/test/resources/config-test.conf
@@ -119,6 +119,7 @@ node {
# maxBlockRange = 5000
# maxSubTopics = 1000
# maxBlockFilterNum = 30000
+ # maxLogFilterNum = 20000
}
# use your ipv6 address for node discovery and tcp connection, default false