From f0a8f0f66fbe09aef1bf1ddf2e50ee0952e730c4 Mon Sep 17 00:00:00 2001 From: 3for Date: Tue, 21 Apr 2026 11:23:51 +0800 Subject: [PATCH 01/43] test(framework): consolidate and stabilize CredentialsTest (#6614) * test(framework): merge duplicate CredentialsTest coverage Consolidate the misplaced keystroe CredentialsTest into org.tron.keystore.CredentialsTest. - remove the duplicate test under the misspelled keystroe package - add explicit equals behavior coverage for address and cryptoEngine - normalize assertions to JUnit Assert and remove legacy TestCase usage * test(framework): stabilize CredentialsTest fixtures Replace random Credentials test setup with deterministic SignInterface mocks so the suite no longer depends on platform-specific SecureRandom providers or probabilistic retries. - remove NativePRNG usage from CredentialsTest - replace random key generation with fixed address fixtures via mocked SignInterface - assert create(SignInterface) returns the expected base58check address - keep equals/hashCode contract coverage with deterministic inputs --- .../org/tron/keystore/CredentialsTest.java | 83 +++++++++++++------ .../org/tron/keystroe/CredentialsTest.java | 33 -------- 2 files changed, 58 insertions(+), 58 deletions(-) delete mode 100644 framework/src/test/java/org/tron/keystroe/CredentialsTest.java diff --git a/framework/src/test/java/org/tron/keystore/CredentialsTest.java b/framework/src/test/java/org/tron/keystore/CredentialsTest.java index 3fe2ce02b63..8aabd887bb0 100644 --- a/framework/src/test/java/org/tron/keystore/CredentialsTest.java +++ b/framework/src/test/java/org/tron/keystore/CredentialsTest.java @@ -1,25 +1,36 @@ package org.tron.keystore; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import junit.framework.TestCase; -import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.junit.Test; -import org.springframework.util.Assert; -import org.tron.common.crypto.SignUtils; +import org.mockito.Mockito; +import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.sm2.SM2; import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.StringUtil; -@Slf4j -public class CredentialsTest extends TestCase { +public class CredentialsTest { + + private static final byte[] ADDRESS_1 = ByteUtil.hexToBytes( + "410102030405060708090a0b0c0d0e0f1011121314"); + private static final byte[] ADDRESS_2 = ByteUtil.hexToBytes( + "411415161718191a1b1c1d1e1f2021222324252627"); + + private SignInterface mockSignInterface(byte[] address) { + SignInterface signInterface = Mockito.mock(SignInterface.class); + Mockito.when(signInterface.getAddress()).thenReturn(address); + return signInterface; + } @Test - public void testCreate() throws NoSuchAlgorithmException { - Credentials credentials = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Assert.hasText(credentials.getAddress(),"Credentials address create failed!"); - Assert.notNull(credentials.getSignInterface(), - "Credentials cryptoEngine create failed"); + public void testCreate() { + SignInterface signInterface = mockSignInterface(ADDRESS_1); + + Credentials credentials = Credentials.create(signInterface); + + Assert.assertEquals("Credentials address create failed!", + StringUtil.encode58Check(ADDRESS_1), credentials.getAddress()); + Assert.assertSame("Credentials cryptoEngine create failed", signInterface, + credentials.getSignInterface()); } @Test @@ -28,21 +39,43 @@ public void testCreateFromSM2() { Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff" + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "fffffffffffffffffffffffffffffffffffffff"))); + Assert.fail("Expected IllegalArgumentException"); } catch (Exception e) { - Assert.isInstanceOf(IllegalArgumentException.class, e); + Assert.assertTrue(e instanceof IllegalArgumentException); } } @Test - public void testEquals() throws NoSuchAlgorithmException { - Credentials credentials1 = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Credentials credentials2 = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Assert.isTrue(!credentials1.equals(credentials2), - "Credentials instance should be not equal!"); - Assert.isTrue(!(credentials1.hashCode() == credentials2.hashCode()), - "Credentials instance hashcode should be not equal!"); + public void testEquals() { + Credentials credentials1 = Credentials.create(mockSignInterface(ADDRESS_1)); + Credentials credentials2 = Credentials.create(mockSignInterface(ADDRESS_2)); + + Assert.assertNotEquals("Credentials address fixtures should differ", + credentials1.getAddress(), credentials2.getAddress()); + Assert.assertNotEquals("Credentials instance should be not equal!", + credentials1, credentials2); + } + + @Test + public void testEqualsWithAddressAndCryptoEngine() { + Object aObject = new Object(); + SignInterface signInterface = mockSignInterface(ADDRESS_1); + SignInterface signInterface2 = mockSignInterface(ADDRESS_1); + SignInterface signInterface3 = mockSignInterface(ADDRESS_2); + + Credentials credential = Credentials.create(signInterface); + Credentials sameCredential = Credentials.create(signInterface); + Credentials sameAddressDifferentEngineCredential = Credentials.create(signInterface2); + Credentials differentCredential = Credentials.create(signInterface3); + + Assert.assertFalse(aObject.equals(credential)); + Assert.assertFalse(credential.equals(aObject)); + Assert.assertFalse(credential.equals(null)); + Assert.assertEquals(credential, sameCredential); + Assert.assertEquals("Equal credentials must have the same hashCode", + credential.hashCode(), sameCredential.hashCode()); + Assert.assertNotEquals(credential, sameAddressDifferentEngineCredential); + Assert.assertFalse(credential.equals(differentCredential)); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java deleted file mode 100644 index 2642129e00a..00000000000 --- a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.tron.keystroe; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; -import org.tron.common.crypto.SignInterface; -import org.tron.keystore.Credentials; - -public class CredentialsTest { - - @Test - public void test_equality() { - Object aObject = new Object(); - SignInterface si = Mockito.mock(SignInterface.class); - SignInterface si2 = Mockito.mock(SignInterface.class); - SignInterface si3 = Mockito.mock(SignInterface.class); - byte[] address = "TQhZ7W1RudxFdzJMw6FvMnujPxrS6sFfmj".getBytes(); - byte[] address2 = "TNCmcTdyrYKMtmE1KU2itzeCX76jGm5Not".getBytes(); - Mockito.when(si.getAddress()).thenReturn(address); - Mockito.when(si2.getAddress()).thenReturn(address); - Mockito.when(si3.getAddress()).thenReturn(address2); - Credentials aCredential = Credentials.create(si); - Assert.assertFalse(aObject.equals(aCredential)); - Assert.assertFalse(aCredential.equals(aObject)); - Assert.assertFalse(aCredential.equals(null)); - Credentials anotherCredential = Credentials.create(si); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential2 = Credentials.create(si2); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential3 = Credentials.create(si3); - Assert.assertFalse(aCredential.equals(aCredential3)); - } -} From aa8006d3c92c5960e02ead188e58f6ecbedcccf3 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:21:24 +0800 Subject: [PATCH 02/43] feat(http): add Jetty SizeLimitHandler to enforce request body size limit Add SizeLimitHandler at the Jetty server level to reject oversized request bodies before they are fully buffered into memory. This prevents OOM attacks via arbitrarily large HTTP payloads that bypass the existing application-level Util.checkBodySize() check (which reads the entire body first) and the JSON-RPC interface (which had no size validation). --- .../tron/common/application/HttpService.java | 6 +- .../org/tron/core/services/http/Util.java | 1 + .../common/jetty/SizeLimitHandlerTest.java | 139 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index e9a902002ba..f3428d37fdb 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.tron.core.config.args.Args; @@ -63,7 +64,10 @@ protected void initServer() { protected ServletContextHandler initContextHandler() { ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath(this.contextPath); - this.apiServer.setHandler(context); + int maxMessageSize = Args.getInstance().getMaxMessageSize(); + SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(maxMessageSize, -1); + sizeLimitHandler.setHandler(context); + this.apiServer.setHandler(sizeLimitHandler); return context; } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..f112e13c818 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -327,6 +327,7 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp } } + @Deprecated public static void checkBodySize(String body) throws Exception { CommonParameter parameter = Args.getInstance(); if (body.getBytes().length > parameter.getMaxMessageSize()) { diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java new file mode 100644 index 00000000000..d06f86963bf --- /dev/null +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -0,0 +1,139 @@ +package org.tron.common.jetty; + +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; +import org.tron.common.utils.PublicMethod; +import org.tron.core.config.args.Args; + +/** + * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size + * enforcement configured in {@link HttpService initContextHandler()}. + * + *

Unlike a standalone Jetty micro-server, this test creates a concrete + * {@link HttpService} subclass and calls {@link HttpService#start()}, so the + * production code paths of {@code initServer()} and {@code initContextHandler()} + * are exercised directly and reflected in code-coverage reports.

+ * + *

Key behaviours proven:

+ * + */ +@Slf4j +public class SizeLimitHandlerTest { + + private static final int MAX_BODY_SIZE = 1024; + + private static TestHttpService httpService; + private static URI serverUri; + private static CloseableHttpClient client; + + /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ + public static class EchoServlet extends HttpServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + byte[] body = ByteStreams.toByteArray(req.getInputStream()); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/octet-stream"); + resp.getOutputStream().write(body); + } + } + + /** Minimal concrete {@link HttpService} that registers only an {@link EchoServlet}. */ + static class TestHttpService extends HttpService { + TestHttpService(int port) { + this.port = port; + this.contextPath = "/"; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new EchoServlet()), "/*"); + } + } + + /** + * Initialises {@link Args} and starts a real {@link HttpService} whose + * {@code initServer()} and {@code initContextHandler()} are the production + * implementations — guaranteeing test coverage of the new + * {@code SizeLimitHandler} wiring. + */ + @BeforeClass + public static void setup() throws Exception { + String dbPath = Files.createTempDirectory("sizelimit-test").toString(); + Args.setParam(new String[]{"--output-directory", dbPath}, TestConstants.TEST_CONF); + Args.getInstance().setMaxMessageSize(MAX_BODY_SIZE); + + int port = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(port); + httpService.start(); + + serverUri = new URI(String.format("http://localhost:%d/", port)); + client = HttpClients.createDefault(); + } + + @AfterClass + public static void teardown() throws Exception { + try { + if (client != null) { + client.close(); + } + } finally { + if (httpService != null) { + httpService.stop(); + } + Args.clearParam(); + } + } + + // -- body-size tests (covers HttpService.initContextHandler) --------------- + + @Test + public void testBodyWithinLimit() throws Exception { + Assert.assertEquals(200, post(new StringEntity("small body"))); + } + + @Test + public void testBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, post(new StringEntity(repeat('a', MAX_BODY_SIZE + 1)))); + } + + // -- helpers --------------------------------------------------------------- + + /** POSTs with the given entity and returns the HTTP status code. */ + private int post(HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(serverUri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + EntityUtils.consume(resp.getEntity()); + return resp.getStatusLine().getStatusCode(); + } + + /** Returns a string of {@code n} repetitions of {@code c}. */ + private static String repeat(char c, int n) { + return new String(new char[n]).replace('\0', c); + } +} From e81cef505295cfbec9a237d800d7de0df431e83b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 19 Mar 2026 15:55:39 +0800 Subject: [PATCH 03/43] feat(http) : wait for HttpService startup future in SizeLimitHandlerTest --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index d06f86963bf..2d174f14629 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Files; +import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -89,7 +90,7 @@ public static void setup() throws Exception { int port = PublicMethod.chooseRandomPort(); httpService = new TestHttpService(port); - httpService.start(); + httpService.start().get(10, TimeUnit.SECONDS); serverUri = new URI(String.format("http://localhost:%d/", port)); client = HttpClients.createDefault(); From 9fc6a29d4dccfefcac5c102ced1c37b865f85af2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 23 Mar 2026 18:31:57 +0800 Subject: [PATCH 04/43] feat(http): add independent maxMessageSize for HTTP and JSON-RPC Introduce node.http.maxMessageSize and node.jsonrpc.maxMessageSize to allow HTTP and JSON-RPC services to enforce separate request body size limits via Jetty SizeLimitHandler, decoupled from gRPC config. - Default: 4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE (16 MB) - Validation: reject <= 0 with TronError(PARAMETER_INIT) at startup - Each HttpService subclass sets its own maxRequestSize in constructor - SizeLimitHandlerTest covers independent limits, boundary, UTF-8 bytes --- .../common/parameter/CommonParameter.java | 6 + .../tron/common/application/HttpService.java | 5 +- .../java/org/tron/core/config/args/Args.java | 14 ++ .../org/tron/core/config/args/ConfigKey.java | 2 + .../services/http/FullNodeHttpApiService.java | 1 + .../solidity/SolidityNodeHttpApiService.java | 1 + .../JsonRpcServiceOnPBFT.java | 1 + .../JsonRpcServiceOnSolidity.java | 1 + .../http/PBFT/HttpApiOnPBFTService.java | 1 + .../solidity/HttpApiOnSolidityService.java | 1 + .../jsonrpc/FullNodeJsonRpcHttpService.java | 1 + .../common/jetty/SizeLimitHandlerTest.java | 143 +++++++++++++----- .../org/tron/core/config/args/ArgsTest.java | 3 + 13 files changed, 144 insertions(+), 36 deletions(-) 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 a73158a718a..739db868281 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -216,6 +216,12 @@ public class CommonParameter { public int maxMessageSize; @Getter @Setter + public int httpMaxMessageSize; + @Getter + @Setter + public int jsonRpcMaxMessageSize; + @Getter + @Setter public int maxHeaderListSize; @Getter @Setter diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index f3428d37fdb..b7032e75589 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -30,6 +30,8 @@ public abstract class HttpService extends AbstractService { protected String contextPath; + protected int maxRequestSize; + @Override public void innerStart() throws Exception { if (this.apiServer != null) { @@ -64,8 +66,7 @@ protected void initServer() { protected ServletContextHandler initContextHandler() { ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath(this.contextPath); - int maxMessageSize = Args.getInstance().getMaxMessageSize(); - SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(maxMessageSize, -1); + SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1); sizeLimitHandler.setHandler(context); this.apiServer.setHandler(sizeLimitHandler); return context; 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 83d7fd2c63d..65cb54bc843 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 @@ -526,6 +526,20 @@ public static void applyConfigParams( PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + int defaultHttpMaxMessageSize = 4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) + ? config.getInt(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + if (PARAMETER.httpMaxMessageSize <= 0) { + throw new TronError("node.http.maxMessageSize must be positive, got: " + + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) + ? config.getInt(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + if (PARAMETER.jsonRpcMaxMessageSize <= 0) { + throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) : GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java index b21c9c440a4..e8a5c504d57 100644 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -136,6 +136,7 @@ private ConfigKey() { public static final String NODE_HTTP_SOLIDITY_ENABLE = "node.http.solidityEnable"; public static final String NODE_HTTP_PBFT_ENABLE = "node.http.PBFTEnable"; public static final String NODE_HTTP_PBFT_PORT = "node.http.PBFTPort"; + public static final String NODE_HTTP_MAX_MESSAGE_SIZE = "node.http.maxMessageSize"; // node - jsonrpc public static final String NODE_JSONRPC_HTTP_FULLNODE_ENABLE = @@ -150,6 +151,7 @@ private ConfigKey() { public static final String NODE_JSONRPC_MAX_SUB_TOPICS = "node.jsonrpc.maxSubTopics"; public static final String NODE_JSONRPC_MAX_BLOCK_FILTER_NUM = "node.jsonrpc.maxBlockFilterNum"; + public static final String NODE_JSONRPC_MAX_MESSAGE_SIZE = "node.jsonrpc.maxMessageSize"; // node - dns public static final String NODE_DNS_TREE_URLS = "node.dns.treeUrls"; diff --git a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java index 3ad4ace62fc..5a3b86cb396 100644 --- a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java @@ -297,6 +297,7 @@ public FullNodeHttpApiService() { port = Args.getInstance().getFullNodeHttpPort(); enable = isFullNode() && Args.getInstance().isFullNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java index 359adfc2b39..0c4843c0550 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java @@ -170,6 +170,7 @@ public SolidityNodeHttpApiService() { port = Args.getInstance().getSolidityHttpPort(); enable = !isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java index fffaf8d4e7b..5282ef5c819 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnPBFT() { port = Args.getInstance().getJsonRpcHttpPBFTPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpPBFTNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java index a6f7d5dd5e7..8b52066d5f8 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnSolidity() { port = Args.getInstance().getJsonRpcHttpSolidityPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpSolidityNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java index a77b45353c9..c0616c2ae78 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java @@ -173,6 +173,7 @@ public HttpApiOnPBFTService() { port = Args.getInstance().getPBFTHttpPort(); enable = isFullNode() && Args.getInstance().isPBFTHttpEnable(); contextPath = "/walletpbft"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java index f69597959f8..33e325bd578 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java @@ -181,6 +181,7 @@ public HttpApiOnSolidityService() { port = Args.getInstance().getSolidityHttpPort(); enable = isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java index 566ad33a722..ffe81bfa100 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java @@ -24,6 +24,7 @@ public FullNodeJsonRpcHttpService() { port = Args.getInstance().getJsonRpcHttpFullNodePort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpFullNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 2d174f14629..dbd6b1eb97e 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -3,7 +3,6 @@ import com.google.common.io.ByteStreams; import java.io.IOException; import java.net.URI; -import java.nio.file.Files; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -21,7 +20,9 @@ import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.tron.common.TestConstants; import org.tron.common.application.HttpService; import org.tron.common.utils.PublicMethod; @@ -29,27 +30,30 @@ /** * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService initContextHandler()}. + * enforcement configured in {@link HttpService#initContextHandler()}. * - *

Unlike a standalone Jetty micro-server, this test creates a concrete - * {@link HttpService} subclass and calls {@link HttpService#start()}, so the - * production code paths of {@code initServer()} and {@code initContextHandler()} - * are exercised directly and reflected in code-coverage reports.

- * - *

Key behaviours proven:

+ *

Covers:

*
    *
  • Bodies within the limit are accepted ({@code 200}).
  • *
  • Bodies exceeding the limit are rejected ({@code 413}).
  • *
  • The limit counts raw UTF-8 bytes, not Java {@code char}s.
  • + *
  • HTTP and JSON-RPC services use independent size limits.
  • + *
  • Default values are 4x {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE}.
  • *
*/ @Slf4j public class SizeLimitHandlerTest { - private static final int MAX_BODY_SIZE = 1024; + private static final int HTTP_MAX_BODY_SIZE = 1024; + private static final int JSONRPC_MAX_BODY_SIZE = 512; + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); private static TestHttpService httpService; - private static URI serverUri; + private static TestJsonRpcService jsonRpcService; + private static URI httpServerUri; + private static URI jsonRpcServerUri; private static CloseableHttpClient client; /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ @@ -63,11 +67,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } - /** Minimal concrete {@link HttpService} that registers only an {@link EchoServlet}. */ + /** Minimal concrete {@link HttpService} wired with a given size limit. */ static class TestHttpService extends HttpService { - TestHttpService(int port) { + TestHttpService(int port, int maxRequestSize) { this.port = port; this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; } @Override @@ -76,23 +81,37 @@ protected void addServlet(ServletContextHandler context) { } } - /** - * Initialises {@link Args} and starts a real {@link HttpService} whose - * {@code initServer()} and {@code initContextHandler()} are the production - * implementations — guaranteeing test coverage of the new - * {@code SizeLimitHandler} wiring. - */ + /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ + static class TestJsonRpcService extends HttpService { + TestJsonRpcService(int port, int maxRequestSize) { + this.port = port; + this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new EchoServlet()), "/jsonrpc"); + } + } + @BeforeClass public static void setup() throws Exception { - String dbPath = Files.createTempDirectory("sizelimit-test").toString(); - Args.setParam(new String[]{"--output-directory", dbPath}, TestConstants.TEST_CONF); - Args.getInstance().setMaxMessageSize(MAX_BODY_SIZE); + Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + Args.getInstance().setHttpMaxMessageSize(HTTP_MAX_BODY_SIZE); + Args.getInstance().setJsonRpcMaxMessageSize(JSONRPC_MAX_BODY_SIZE); - int port = PublicMethod.chooseRandomPort(); - httpService = new TestHttpService(port); + int httpPort = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(httpPort, HTTP_MAX_BODY_SIZE); httpService.start().get(10, TimeUnit.SECONDS); + httpServerUri = new URI(String.format("http://localhost:%d/", httpPort)); + + int jsonRpcPort = PublicMethod.chooseRandomPort(); + jsonRpcService = new TestJsonRpcService(jsonRpcPort, JSONRPC_MAX_BODY_SIZE); + jsonRpcService.start().get(10, TimeUnit.SECONDS); + jsonRpcServerUri = new URI(String.format("http://localhost:%d/jsonrpc", jsonRpcPort)); - serverUri = new URI(String.format("http://localhost:%d/", port)); client = HttpClients.createDefault(); } @@ -103,30 +122,86 @@ public static void teardown() throws Exception { client.close(); } } finally { - if (httpService != null) { - httpService.stop(); + try { + if (httpService != null) { + httpService.stop(); + } + } finally { + if (jsonRpcService != null) { + jsonRpcService.stop(); + } } Args.clearParam(); } } - // -- body-size tests (covers HttpService.initContextHandler) --------------- + // -- HTTP service body-size tests ------------------------------------------- + + @Test + public void testHttpBodyWithinLimit() throws Exception { + Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); + } + + @Test + public void testHttpBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, + post(httpServerUri, new StringEntity(repeat('a', HTTP_MAX_BODY_SIZE + 1)))); + } + + @Test + public void testHttpBodyAtExactLimit() throws Exception { + Assert.assertEquals(200, + post(httpServerUri, new StringEntity(repeat('b', HTTP_MAX_BODY_SIZE)))); + } + + // -- JSON-RPC service body-size tests --------------------------------------- @Test - public void testBodyWithinLimit() throws Exception { - Assert.assertEquals(200, post(new StringEntity("small body"))); + public void testJsonRpcBodyWithinLimit() throws Exception { + Assert.assertEquals(200, + post(jsonRpcServerUri, new StringEntity("{\"method\":\"eth_blockNumber\"}"))); } @Test - public void testBodyExceedsLimit() throws Exception { - Assert.assertEquals(413, post(new StringEntity(repeat('a', MAX_BODY_SIZE + 1)))); + public void testJsonRpcBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, + post(jsonRpcServerUri, new StringEntity(repeat('x', JSONRPC_MAX_BODY_SIZE + 1)))); + } + + @Test + public void testJsonRpcBodyAtExactLimit() throws Exception { + Assert.assertEquals(200, + post(jsonRpcServerUri, new StringEntity(repeat('c', JSONRPC_MAX_BODY_SIZE)))); + } + + // -- Independent limit tests ------------------------------------------------ + + @Test + public void testHttpAndJsonRpcHaveIndependentLimits() throws Exception { + // A body that exceeds JSON-RPC limit but is within HTTP limit + String body = repeat('d', JSONRPC_MAX_BODY_SIZE + 100); + Assert.assertTrue(body.length() < HTTP_MAX_BODY_SIZE); + + Assert.assertEquals(200, post(httpServerUri, new StringEntity(body))); + Assert.assertEquals(413, post(jsonRpcServerUri, new StringEntity(body))); + } + + // -- UTF-8 byte counting test ----------------------------------------------- + + @Test + public void testLimitIsBasedOnBytesNotCharacters() throws Exception { + // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 + String cjk = repeat('\u4e00', 342); + Assert.assertEquals(342, cjk.length()); + Assert.assertEquals(1026, cjk.getBytes("UTF-8").length); + Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); } - // -- helpers --------------------------------------------------------------- + // -- helpers ---------------------------------------------------------------- /** POSTs with the given entity and returns the HTTP status code. */ - private int post(HttpEntity entity) throws Exception { - HttpPost req = new HttpPost(serverUri); + private int post(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); req.setEntity(entity); HttpResponse resp = client.execute(req); EntityUtils.consume(resp.getEntity()); diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 88d893bcf97..a1851f87550 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -119,6 +119,9 @@ public void get() { Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getMaxMessageSize()); + Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); + Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, + parameter.getJsonRpcMaxMessageSize()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, parameter.getMaxHeaderListSize()); Assert.assertEquals(1L, parameter.getAllowCreationOfContracts()); Assert.assertEquals(0, parameter.getConsensusLogicOptimization()); From adb49d6f8057ee7f9f8e97c34901ec45fda78919 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 1 Apr 2026 20:52:39 +0800 Subject: [PATCH 05/43] opt(checkstyle): optimize checkstyle --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index dbd6b1eb97e..e12b6ea57ad 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -30,7 +30,7 @@ /** * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService#initContextHandler()}. + * enforcement configured in {@link HttpService initContextHandler()}. * *

Covers:

*
    @@ -191,7 +191,7 @@ public void testHttpAndJsonRpcHaveIndependentLimits() throws Exception { @Test public void testLimitIsBasedOnBytesNotCharacters() throws Exception { // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 - String cjk = repeat('\u4e00', 342); + String cjk = repeat('一', 342); Assert.assertEquals(342, cjk.length()); Assert.assertEquals(1026, cjk.getBytes("UTF-8").length); Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); From 2cc14e25d2e68bf3729612bb6af73e78ef0c41d1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 09:45:59 +0800 Subject: [PATCH 06/43] change(config): update default size --- framework/src/main/java/org/tron/core/config/args/Args.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 65cb54bc843..0f2d4c9190b 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 @@ -526,7 +526,7 @@ public static void applyConfigParams( PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - int defaultHttpMaxMessageSize = 4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + int defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getInt(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; if (PARAMETER.httpMaxMessageSize <= 0) { From e182fb27efea99905b6c937dfc8478c5af03a6dc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 2 Apr 2026 16:08:22 +0800 Subject: [PATCH 07/43] test(framework): align ArgsTest with 4M defaults --- .../src/test/java/org/tron/core/config/args/ArgsTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index a1851f87550..1bed3e1c6ad 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -119,9 +119,8 @@ public void get() { Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getMaxMessageSize()); - Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); - Assert.assertEquals(4 * GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, - parameter.getJsonRpcMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getJsonRpcMaxMessageSize()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, parameter.getMaxHeaderListSize()); Assert.assertEquals(1L, parameter.getAllowCreationOfContracts()); Assert.assertEquals(0, parameter.getConsensusLogicOptimization()); @@ -364,4 +363,3 @@ public void testConfigStorageDefaults() { Args.clearParam(); } } - From e0df7d4768245d6eeeabcc547ded0f6976540b76 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 3 Apr 2026 15:48:08 +0800 Subject: [PATCH 08/43] test(http): doc for default value --- .../test/java/org/tron/common/jetty/SizeLimitHandlerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index e12b6ea57ad..a2c4c3f369c 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -38,7 +38,7 @@ *
  • Bodies exceeding the limit are rejected ({@code 413}).
  • *
  • The limit counts raw UTF-8 bytes, not Java {@code char}s.
  • *
  • HTTP and JSON-RPC services use independent size limits.
  • - *
  • Default values are 4x {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE}.
  • + *
  • Default values are {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE} (4 MB).
  • *
*/ @Slf4j From baa4d88431e108127a1d341813e8eedb37fb12bb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 9 Apr 2026 18:34:59 +0800 Subject: [PATCH 09/43] fix(api): use httpMaxMessageSize in checkBodySize instead of gRPC limit checkBodySize() was enforcing maxMessageSize (gRPC limit) instead of httpMaxMessageSize, causing the independent HTTP size setting to be ineffective at the servlet layer. --- .../org/tron/core/services/http/Util.java | 5 ++-- .../org/tron/core/services/http/UtilTest.java | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index f112e13c818..1fefdde5e91 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -330,8 +330,9 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp @Deprecated public static void checkBodySize(String body) throws Exception { CommonParameter parameter = Args.getInstance(); - if (body.getBytes().length > parameter.getMaxMessageSize()) { - throw new Exception("body size is too big, the limit is " + parameter.getMaxMessageSize()); + if (body.getBytes().length > parameter.getHttpMaxMessageSize()) { + throw new Exception("body size is too big, the limit is " + + parameter.getHttpMaxMessageSize()); } } diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 98c11fd4018..0a8337fee66 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -129,6 +129,32 @@ public void testPackTransactionWithInvalidType() { txSignWeight.getResult().getMessage()); } + @Test + public void testCheckBodySizeUsesHttpLimit() throws Exception { + int originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); + int originalRpcMax = Args.getInstance().getMaxMessageSize(); + try { + // set httpMaxMessageSize larger than maxMessageSize + Args.getInstance().setHttpMaxMessageSize(200); + Args.getInstance().setMaxMessageSize(100); + + String withinHttpLimit = new String(new char[150]).replace('\0', 'a'); + // should pass: 150 < httpMaxMessageSize(200), even though > maxMessageSize(100) + Util.checkBodySize(withinHttpLimit); + + String exceedsHttpLimit = new String(new char[201]).replace('\0', 'b'); + try { + Util.checkBodySize(exceedsHttpLimit); + Assert.fail("expected exception for body exceeding httpMaxMessageSize"); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("200")); + } + } finally { + Args.getInstance().setHttpMaxMessageSize(originalHttpMax); + Args.getInstance().setMaxMessageSize(originalRpcMax); + } + } + @Test public void testPackTransaction() { String strTransaction = "{\n" From cd1ebad625a6586591213aab4e4af4b234af2d0b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 17:22:56 +0800 Subject: [PATCH 10/43] test(http): add chunked transfer and zero-limit tests for SizeLimitHandler Add tests to cover scenarios raised in PR review: - Chunked (no Content-Length) requests within/exceeding the limit - Servlet broad catch(Exception) absorbing streaming BadMessageException - SizeLimitHandler behavior when limit is 0 (rejects all non-empty bodies) Replace EchoServlet with BroadCatchServlet to mirror real servlet chain. --- .../common/jetty/SizeLimitHandlerTest.java | 97 +++++++++++++++++-- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index a2c4c3f369c..12cd78c3b9a 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -1,9 +1,10 @@ package org.tron.common.jetty; -import com.google.common.io.ByteStreams; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -11,6 +12,7 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -56,14 +58,25 @@ public class SizeLimitHandlerTest { private static URI jsonRpcServerUri; private static CloseableHttpClient client; - /** Echoes the raw request-body bytes back so tests can inspect what arrived. */ - public static class EchoServlet extends HttpServlet { + /** + * Simulates the real servlet pattern: reads body via getReader(), wraps in + * broad catch(Exception) — mirrors what RateLimiterServlet + actual servlets do. + */ + public static class BroadCatchServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - byte[] body = ByteStreams.toByteArray(req.getInputStream()); - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("application/octet-stream"); - resp.getOutputStream().write(body); + try { + String body = req.getReader().lines() + .collect(Collectors.joining(System.lineSeparator())); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"size\":" + body.length() + "}"); + } catch (Exception e) { + // Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"Error\":\"" + e.getClass().getSimpleName() + "\"}"); + } } } @@ -77,7 +90,7 @@ static class TestHttpService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new EchoServlet()), "/*"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); } } @@ -91,7 +104,7 @@ static class TestJsonRpcService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new EchoServlet()), "/jsonrpc"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/jsonrpc"); } } @@ -197,6 +210,72 @@ public void testLimitIsBasedOnBytesNotCharacters() throws Exception { Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); } + // -- Chunked (no Content-Length) transfer tests ------------------------------ + + /** + * Chunked request within the limit should succeed (EchoServlet). + * InputStreamEntity with size=-1 sends chunked Transfer-Encoding (no Content-Length). + */ + @Test + public void testChunkedBodyWithinLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE / 4).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + Assert.assertEquals(200, post(httpServerUri, chunked)); + } + + /** + * Chunked oversized body hitting a servlet with broad catch(Exception). + * + *

SizeLimitHandler's LimitInterceptor throws BadMessageException during + * streaming read, but the servlet's catch(Exception) absorbs it and returns + * 200 + error JSON instead of 413. This matches real TRON servlet behavior. + * + *

OOM protection still works: the body read is truncated at the limit. + */ + @Test + public void testChunkedBodyExceedsLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE * 2).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + HttpPost req = new HttpPost(httpServerUri); + req.setEntity(chunked); + HttpResponse resp = client.execute(req); + int status = resp.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(resp.getEntity()); + logger.info("Chunked oversized: status={}, body={}", status, body); + + // catch(Exception) absorbs BadMessageException → 200 + error JSON, not 413. + // Body read IS truncated — OOM protection still effective. + Assert.assertEquals(200, status); + Assert.assertTrue("Error should be surfaced in response body", + body.contains("Error")); + } + + // -- Zero-limit behavior test ----------------------------------------------- + + /** + * When maxRequestSize is 0, SizeLimitHandler treats it as "reject all bodies > 0 bytes". + * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} — 0 >= 0 is true, + * so any non-empty body triggers 413. This is NOT "pass all" — it is a silent DoS + * against the node's own API. + */ + @Test + public void testZeroLimitRejectsAllBodies() throws Exception { + int zeroPort = PublicMethod.chooseRandomPort(); + TestHttpService zeroService = new TestHttpService(zeroPort, 0); + try { + zeroService.start().get(10, TimeUnit.SECONDS); + URI zeroUri = new URI(String.format("http://localhost:%d/", zeroPort)); + + // Empty body should pass (0 is NOT > 0) + Assert.assertEquals(200, post(zeroUri, new StringEntity(""))); + + // Any non-empty body should be rejected + Assert.assertEquals(413, post(zeroUri, new StringEntity("x"))); + } finally { + zeroService.stop(); + } + } + // -- helpers ---------------------------------------------------------------- /** POSTs with the given entity and returns the HTTP status code. */ From 8c539d13347f4b40238c8c9d76f59427e08a0417 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:02:31 +0800 Subject: [PATCH 11/43] refactor(config): use getMemorySize() for size limit configs Replace getInt() with getMemorySize().toBytes() for maxMessageSize, httpMaxMessageSize and jsonRpcMaxMessageSize config parsing. This enables human-readable size values (e.g. 4m, 128MB) while maintaining backward compatibility with raw byte values. - maxMessageSize (gRPC): keep int field, validate <= Integer.MAX_VALUE for gRPC API compatibility, add positive value validation - httpMaxMessageSize / jsonRpcMaxMessageSize: widen to long, matching Jetty SizeLimitHandler's long parameter type - Add config.conf documentation for all three size parameters --- .../tron/common/parameter/CommonParameter.java | 4 ++-- .../tron/common/application/HttpService.java | 2 +- .../java/org/tron/core/config/args/Args.java | 18 +++++++++++++----- framework/src/main/resources/config.conf | 13 +++++++++++-- .../common/jetty/SizeLimitHandlerTest.java | 4 ++-- .../org/tron/core/services/http/UtilTest.java | 2 +- 6 files changed, 30 insertions(+), 13 deletions(-) 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 739db868281..53c5d4cfaca 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -216,10 +216,10 @@ public class CommonParameter { public int maxMessageSize; @Getter @Setter - public int httpMaxMessageSize; + public long httpMaxMessageSize; @Getter @Setter - public int jsonRpcMaxMessageSize; + public long jsonRpcMaxMessageSize; @Getter @Setter public int maxHeaderListSize; diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index b7032e75589..388a524b633 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -30,7 +30,7 @@ public abstract class HttpService extends AbstractService { protected String contextPath; - protected int maxRequestSize; + protected long maxRequestSize; @Override public void innerStart() throws Exception { 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 0f2d4c9190b..c7738ca80d2 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 @@ -523,18 +523,26 @@ public static void applyConfigParams( ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) : Long.MAX_VALUE; - PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long rpcMaxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) + ? config.getMemorySize(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE).toBytes() + : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + if (rpcMaxMessageSize <= 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.rpc.maxMessageSize must be positive and <= " + + Integer.MAX_VALUE + ", got: " + rpcMaxMessageSize, PARAMETER_INIT); + } + PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; - int defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() + : defaultHttpMaxMessageSize; if (PARAMETER.httpMaxMessageSize <= 0) { throw new TronError("node.http.maxMessageSize must be positive, got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) : defaultHttpMaxMessageSize; + ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() + : defaultHttpMaxMessageSize; if (PARAMETER.jsonRpcMaxMessageSize <= 0) { throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..abb6b2f7ec2 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -223,6 +223,10 @@ node { solidityPort = 8091 PBFTEnable = true PBFTPort = 8092 + + # The maximum request body size for HTTP API, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m } rpc { @@ -248,8 +252,9 @@ node { # Connection lasting longer than which will be gracefully terminated # maxConnectionAgeInMillis = - # The maximum message size allowed to be received on the server, default 4MB - # maxMessageSize = + # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 # maxHeaderListSize = @@ -357,6 +362,10 @@ node { # openHistoryQueryWhenLiteFN = false jsonrpc { + # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # maxMessageSize = 4m + # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, # you will not be able to get the data from eth_getLogs for that period of time. Default: false # httpFullNodeEnable = false diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 12cd78c3b9a..13c840fc2e3 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -82,7 +82,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I /** Minimal concrete {@link HttpService} wired with a given size limit. */ static class TestHttpService extends HttpService { - TestHttpService(int port, int maxRequestSize) { + TestHttpService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; @@ -96,7 +96,7 @@ protected void addServlet(ServletContextHandler context) { /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ static class TestJsonRpcService extends HttpService { - TestJsonRpcService(int port, int maxRequestSize) { + TestJsonRpcService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 0a8337fee66..24a8a2bdef3 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -131,7 +131,7 @@ public void testPackTransactionWithInvalidType() { @Test public void testCheckBodySizeUsesHttpLimit() throws Exception { - int originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); + long originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); int originalRpcMax = Args.getInstance().getMaxMessageSize(); try { // set httpMaxMessageSize larger than maxMessageSize From 757470c9c0cb6a1e6be7ed3f082fc7d1cf598781 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:06:31 +0800 Subject: [PATCH 12/43] fix(config): allow zero value for maxMessageSize parameters Change validation from <= 0 to < 0 for all three size limit configs. Zero is a valid value meaning "reject all non-empty request bodies", with consistent semantics in both gRPC (maxInboundMessageSize >= 0) and Jetty SizeLimitHandler (limit >= 0 && size > limit). --- .../main/java/org/tron/core/config/args/Args.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 c7738ca80d2..bc30a1f0a39 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 @@ -526,8 +526,8 @@ public static void applyConfigParams( long rpcMaxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE).toBytes() : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - if (rpcMaxMessageSize <= 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { - throw new TronError("node.rpc.maxMessageSize must be positive and <= " + if (rpcMaxMessageSize < 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.rpc.maxMessageSize must be non-negative and <= " + Integer.MAX_VALUE + ", got: " + rpcMaxMessageSize, PARAMETER_INIT); } PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; @@ -536,15 +536,15 @@ public static void applyConfigParams( PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() : defaultHttpMaxMessageSize; - if (PARAMETER.httpMaxMessageSize <= 0) { - throw new TronError("node.http.maxMessageSize must be positive, got: " + if (PARAMETER.httpMaxMessageSize < 0) { + throw new TronError("node.http.maxMessageSize must be non-negative, got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() : defaultHttpMaxMessageSize; - if (PARAMETER.jsonRpcMaxMessageSize <= 0) { - throw new TronError("node.jsonrpc.maxMessageSize must be positive, got: " + if (PARAMETER.jsonRpcMaxMessageSize < 0) { + throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); } From 0b45cbbae92fe1c2b7b03b3cb3ca631719704ef9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:25:34 +0800 Subject: [PATCH 13/43] test(http): verify checkBodySize consistency with SizeLimitHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests proving the two enforcement layers measure identical byte counts for normal JSON payloads (ASCII and UTF-8). For bodies with \r\n line endings, checkBodySize measures fewer bytes than wire — a safe direction that never causes false rejections. --- .../common/jetty/SizeLimitHandlerTest.java | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 13c840fc2e3..7c264cc8fdb 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -1,5 +1,6 @@ package org.tron.common.jetty; +import com.alibaba.fastjson.JSONObject; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; @@ -70,7 +71,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I .collect(Collectors.joining(System.lineSeparator())); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType("application/json"); - resp.getWriter().println("{\"size\":" + body.length() + "}"); + resp.getWriter().println("{\"size\":" + body.length() + + ",\"bytes\":" + body.getBytes().length + "}"); } catch (Exception e) { // Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow resp.setStatus(HttpServletResponse.SC_OK); @@ -276,8 +278,76 @@ public void testZeroLimitRejectsAllBodies() throws Exception { } } + // -- checkBodySize vs SizeLimitHandler consistency tests -------------------- + + /** + * For pure ASCII JSON (the normal TRON API case), wire bytes and + * {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures) + * must be identical — the two enforcement layers agree exactly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { + String jsonBody = "{\"owner_address\":\"TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz\"" + + ",\"amount\":1000000}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for ASCII JSON", + wireBytes, servletBytes); + } + + /** + * For UTF-8 JSON with multi-byte characters (CJK), wire bytes and + * {@code body.getBytes().length} must still be identical — UTF-8 round-trips + * through {@code request.getReader()} → {@code String.getBytes()} losslessly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { + String jsonBody = "{\"name\":\"测试地址\",\"amount\":100}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for UTF-8 JSON", + wireBytes, servletBytes); + } + + /** + * When the body contains {@code \r\n} line endings, {@code lines().collect()} + * normalizes them to {@code \n} (on Linux) or the platform line separator. + * This makes {@code checkBodySize} measure fewer bytes than the wire — + * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. + */ + @Test + public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception { + String body = "{\"key1\":\"value1\",\r\n\"key2\":\"value2\",\r\n\"key3\":\"value3\"}"; + int wireBytes = body.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(body, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertTrue("checkBodySize bytes <= wire bytes (safe direction)", + servletBytes <= wireBytes); + logger.info("Newline test: wire={}, servlet={}, diff={}", + wireBytes, servletBytes, wireBytes - servletBytes); + } + // -- helpers ---------------------------------------------------------------- + /** POSTs with the given entity and returns the response body as a string. */ + private String postForBody(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + return EntityUtils.toString(resp.getEntity()); + } + /** POSTs with the given entity and returns the HTTP status code. */ private int post(URI uri, HttpEntity entity) throws Exception { HttpPost req = new HttpPost(uri); From c28405ca5465a33721e4cc36faa3df9285f44828 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 10 Apr 2026 18:37:26 +0800 Subject: [PATCH 14/43] fix(config): correct comment from "positive" to "non-negative" Zero is a valid value for all three maxMessageSize parameters, so the config documentation should say "non-negative" not "positive". --- framework/src/main/resources/config.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index abb6b2f7ec2..dcd399efa83 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -225,7 +225,7 @@ node { PBFTPort = 8092 # The maximum request body size for HTTP API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m } @@ -253,7 +253,7 @@ node { # maxConnectionAgeInMillis = # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 @@ -363,7 +363,7 @@ node { jsonrpc { # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be positive. + # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. # maxMessageSize = 4m # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, From c8bacc838ce0b6893225c2f7f8096129d8d78753 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 14 Apr 2026 17:55:42 +0800 Subject: [PATCH 15/43] fix(http): add safe default for maxRequestSize and fix misleading variable name Initialize HttpService.maxRequestSize to 4MB so future subclasses that omit the assignment get a safe limit instead of 0 (reject all requests). Rename defaultHttpMaxMessageSize to defaultMaxMessageSize since it serves as the fallback for both HTTP and JSON-RPC limits. --- .../main/java/org/tron/common/application/HttpService.java | 2 +- framework/src/main/java/org/tron/core/config/args/Args.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 388a524b633..66a21a8a4ed 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -30,7 +30,7 @@ public abstract class HttpService extends AbstractService { protected String contextPath; - protected long maxRequestSize; + protected long maxRequestSize = 4 * 1024 * 1024; // 4MB @Override public void innerStart() throws Exception { 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 bc30a1f0a39..ff20ed30241 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 @@ -532,17 +532,17 @@ public static void applyConfigParams( } PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; - long defaultHttpMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; + long defaultMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() - : defaultHttpMaxMessageSize; + : defaultMaxMessageSize; if (PARAMETER.httpMaxMessageSize < 0) { throw new TronError("node.http.maxMessageSize must be non-negative, got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() - : defaultHttpMaxMessageSize; + : defaultMaxMessageSize; if (PARAMETER.jsonRpcMaxMessageSize < 0) { throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); From 8938c520ccea0ba0b6364faf90e8bfb57cc1654f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 14 Apr 2026 18:31:46 +0800 Subject: [PATCH 16/43] test(config): add maxMessageSize parsing and zero-value documentation Add comprehensive ArgsTest coverage for getMemorySize() parsing: - Human-readable sizes across KB/MB/GB tiers (binary and SI units) - Raw integer backward compatibility - Zero-value acceptance - Error paths: exceeds int max, negative value, invalid unit, non-numeric Document zero-value behavior in config.conf for all three maxMessageSize entries: "Setting to 0 rejects all non-empty request bodies". --- framework/src/main/resources/config.conf | 3 + .../org/tron/core/config/args/ArgsTest.java | 162 ++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index dcd399efa83..8fc779546c3 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -226,6 +226,7 @@ node { # The maximum request body size for HTTP API, default 4M (4194304 bytes). # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m } @@ -254,6 +255,7 @@ node { # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m # The maximum size of header list allowed to be received, default 8192 @@ -364,6 +366,7 @@ node { jsonrpc { # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 1bed3e1c6ad..63878f21740 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -17,6 +17,7 @@ import com.google.common.collect.Lists; import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; import io.grpc.internal.GrpcUtil; import io.grpc.netty.NettyServerBuilder; @@ -39,6 +40,7 @@ import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; import org.tron.core.config.Configuration; +import org.tron.core.exception.TronError; @Slf4j public class ArgsTest { @@ -362,4 +364,164 @@ public void testConfigStorageDefaults() { Args.clearParam(); } + + @Test + public void testMaxMessageSizeHumanReadable() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + + // --- KB tier: binary (k/K/Ki/KiB = 1024) vs SI (kB = 1000) --- + configMap.put("node.rpc.maxMessageSize", "512k"); + configMap.put("node.http.maxMessageSize", "512K"); + configMap.put("node.jsonrpc.maxMessageSize", "512kB"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(512 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(512 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(512 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "256Ki"); + configMap.put("node.http.maxMessageSize", "256KiB"); + configMap.put("node.jsonrpc.maxMessageSize", "256kB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(256 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(256 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(256 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- MB tier: binary (m/M/Mi/MiB = 1024*1024) vs SI (MB = 1000*1000) --- + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "8M"); + configMap.put("node.jsonrpc.maxMessageSize", "2MB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(8 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(2 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "4Mi"); + configMap.put("node.http.maxMessageSize", "4MiB"); + configMap.put("node.jsonrpc.maxMessageSize", "4MB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(4 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- GB tier: binary (g/G/Gi/GiB) vs SI (GB) --- + // rpc is int-bounded, only test http/jsonrpc for GB values + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "1g"); + configMap.put("node.jsonrpc.maxMessageSize", "1GB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(1024L * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(1000L * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + configMap.put("node.rpc.maxMessageSize", "4m"); + configMap.put("node.http.maxMessageSize", "2Gi"); + configMap.put("node.jsonrpc.maxMessageSize", "2GiB"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- raw integer (backward compatible): treated as bytes --- + configMap.put("node.rpc.maxMessageSize", "4194304"); + configMap.put("node.http.maxMessageSize", "4194304"); + configMap.put("node.jsonrpc.maxMessageSize", "4194304"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + + // --- zero is allowed --- + configMap.put("node.rpc.maxMessageSize", "0"); + configMap.put("node.http.maxMessageSize", "0"); + configMap.put("node.jsonrpc.maxMessageSize", "0"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + Args.applyConfigParams(config); + Assert.assertEquals(0, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + } + + @Test + public void testRpcMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "3g"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected TronError for rpc maxMessageSize > Integer.MAX_VALUE"); + } catch (TronError e) { + Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); + } + } + + @Test + public void testMaxMessageSizeNegativeValue() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "-4m"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected IllegalArgumentException for negative memory size"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains("negative")); + } + } + + @Test + public void testMaxMessageSizeInvalidUnit() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.rpc.maxMessageSize", "4x"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected ConfigException.BadValue for invalid unit"); + } catch (ConfigException.BadValue e) { + Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); + } + } + + @Test + public void testMaxMessageSizeNonNumeric() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.http.maxMessageSize", "abc"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + try { + Args.applyConfigParams(config); + Assert.fail("expected ConfigException.BadValue for non-numeric value"); + } catch (ConfigException.BadValue e) { + Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); + } + } } From aea0be80b578a1dd61648f3978590600192e8907 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 15 Apr 2026 15:04:40 +0800 Subject: [PATCH 17/43] test(http): add real JSON-RPC integration test and clean up SizeLimitHandler tests Add testJsonRpcSizeLimitIntegration() in JsonrpcServiceTest using the Spring-injected FullNodeJsonRpcHttpService (real JsonRpcServlet + jsonrpc4j) to verify SizeLimitHandler does not introduce regressions. Covers: normal request passthrough, Content-Length oversized 413, and chunked oversized behavior (200 empty body due to RateLimiterServlet absorbing BadMessageException). Clean up SizeLimitHandlerTest: remove 3 redundant testJsonRpcBody* tests that used BroadCatchServlet (cannot represent real jsonrpc4j chain), rename TestJsonRpcService to SecondHttpService, remove banner-style ruler comments, fix stale EchoServlet Javadoc reference, and remove HTML tags from Javadoc. --- .../common/jetty/SizeLimitHandlerTest.java | 99 ++++++------------- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 74 ++++++++++++++ 2 files changed, 105 insertions(+), 68 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 7c264cc8fdb..d3f60f9b119 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -32,31 +32,28 @@ import org.tron.core.config.args.Args; /** - * Tests the {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService initContextHandler()}. + * Tests {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size + * enforcement configured in {@link HttpService#initContextHandler()}. * - *

Covers:

- *
    - *
  • Bodies within the limit are accepted ({@code 200}).
  • - *
  • Bodies exceeding the limit are rejected ({@code 413}).
  • - *
  • The limit counts raw UTF-8 bytes, not Java {@code char}s.
  • - *
  • HTTP and JSON-RPC services use independent size limits.
  • - *
  • Default values are {@code GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE} (4 MB).
  • - *
+ * Covers: accept/reject by size, UTF-8 byte counting, independent limits + * across HttpService instances, chunked transfer, and zero-limit behavior. + * + * Real JsonRpcServlet integration is tested separately in + * {@code JsonrpcServiceTest#testJsonRpcSizeLimitIntegration}. */ @Slf4j public class SizeLimitHandlerTest { private static final int HTTP_MAX_BODY_SIZE = 1024; - private static final int JSONRPC_MAX_BODY_SIZE = 512; + private static final int SECOND_SERVICE_MAX_BODY_SIZE = 512; @ClassRule public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private static TestHttpService httpService; - private static TestJsonRpcService jsonRpcService; - private static URI httpServerUri; - private static URI jsonRpcServerUri; + private static TestHttpService httpService; + private static SecondHttpService secondService; + private static URI httpServerUri; + private static URI secondServerUri; private static CloseableHttpClient client; /** @@ -96,9 +93,9 @@ protected void addServlet(ServletContextHandler context) { } } - /** Minimal concrete {@link HttpService} simulating a JSON-RPC service. */ - static class TestJsonRpcService extends HttpService { - TestJsonRpcService(int port, long maxRequestSize) { + /** Second HttpService instance with a different size limit, for independence tests. */ + static class SecondHttpService extends HttpService { + SecondHttpService(int port, long maxRequestSize) { this.port = port; this.contextPath = "/"; this.maxRequestSize = maxRequestSize; @@ -106,7 +103,7 @@ static class TestJsonRpcService extends HttpService { @Override protected void addServlet(ServletContextHandler context) { - context.addServlet(new ServletHolder(new BroadCatchServlet()), "/jsonrpc"); + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); } } @@ -115,17 +112,17 @@ public static void setup() throws Exception { Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, TestConstants.TEST_CONF); Args.getInstance().setHttpMaxMessageSize(HTTP_MAX_BODY_SIZE); - Args.getInstance().setJsonRpcMaxMessageSize(JSONRPC_MAX_BODY_SIZE); + Args.getInstance().setJsonRpcMaxMessageSize(SECOND_SERVICE_MAX_BODY_SIZE); int httpPort = PublicMethod.chooseRandomPort(); httpService = new TestHttpService(httpPort, HTTP_MAX_BODY_SIZE); httpService.start().get(10, TimeUnit.SECONDS); httpServerUri = new URI(String.format("http://localhost:%d/", httpPort)); - int jsonRpcPort = PublicMethod.chooseRandomPort(); - jsonRpcService = new TestJsonRpcService(jsonRpcPort, JSONRPC_MAX_BODY_SIZE); - jsonRpcService.start().get(10, TimeUnit.SECONDS); - jsonRpcServerUri = new URI(String.format("http://localhost:%d/jsonrpc", jsonRpcPort)); + int secondPort = PublicMethod.chooseRandomPort(); + secondService = new SecondHttpService(secondPort, SECOND_SERVICE_MAX_BODY_SIZE); + secondService.start().get(10, TimeUnit.SECONDS); + secondServerUri = new URI(String.format("http://localhost:%d/", secondPort)); client = HttpClients.createDefault(); } @@ -142,16 +139,14 @@ public static void teardown() throws Exception { httpService.stop(); } } finally { - if (jsonRpcService != null) { - jsonRpcService.stop(); + if (secondService != null) { + secondService.stop(); } } Args.clearParam(); } } - // -- HTTP service body-size tests ------------------------------------------- - @Test public void testHttpBodyWithinLimit() throws Exception { Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); @@ -169,40 +164,16 @@ public void testHttpBodyAtExactLimit() throws Exception { post(httpServerUri, new StringEntity(repeat('b', HTTP_MAX_BODY_SIZE)))); } - // -- JSON-RPC service body-size tests --------------------------------------- - - @Test - public void testJsonRpcBodyWithinLimit() throws Exception { - Assert.assertEquals(200, - post(jsonRpcServerUri, new StringEntity("{\"method\":\"eth_blockNumber\"}"))); - } - - @Test - public void testJsonRpcBodyExceedsLimit() throws Exception { - Assert.assertEquals(413, - post(jsonRpcServerUri, new StringEntity(repeat('x', JSONRPC_MAX_BODY_SIZE + 1)))); - } - - @Test - public void testJsonRpcBodyAtExactLimit() throws Exception { - Assert.assertEquals(200, - post(jsonRpcServerUri, new StringEntity(repeat('c', JSONRPC_MAX_BODY_SIZE)))); - } - - // -- Independent limit tests ------------------------------------------------ - @Test - public void testHttpAndJsonRpcHaveIndependentLimits() throws Exception { - // A body that exceeds JSON-RPC limit but is within HTTP limit - String body = repeat('d', JSONRPC_MAX_BODY_SIZE + 100); + public void testTwoServicesHaveIndependentLimits() throws Exception { + // A body that exceeds secondService limit but is within httpService limit + String body = repeat('d', SECOND_SERVICE_MAX_BODY_SIZE + 100); Assert.assertTrue(body.length() < HTTP_MAX_BODY_SIZE); Assert.assertEquals(200, post(httpServerUri, new StringEntity(body))); - Assert.assertEquals(413, post(jsonRpcServerUri, new StringEntity(body))); + Assert.assertEquals(413, post(secondServerUri, new StringEntity(body))); } - // -- UTF-8 byte counting test ----------------------------------------------- - @Test public void testLimitIsBasedOnBytesNotCharacters() throws Exception { // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 @@ -212,10 +183,8 @@ public void testLimitIsBasedOnBytesNotCharacters() throws Exception { Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); } - // -- Chunked (no Content-Length) transfer tests ------------------------------ - /** - * Chunked request within the limit should succeed (EchoServlet). + * Chunked request within the limit should succeed. * InputStreamEntity with size=-1 sends chunked Transfer-Encoding (no Content-Length). */ @Test @@ -228,11 +197,11 @@ public void testChunkedBodyWithinLimit() throws Exception { /** * Chunked oversized body hitting a servlet with broad catch(Exception). * - *

SizeLimitHandler's LimitInterceptor throws BadMessageException during + * SizeLimitHandler's LimitInterceptor throws BadMessageException during * streaming read, but the servlet's catch(Exception) absorbs it and returns * 200 + error JSON instead of 413. This matches real TRON servlet behavior. * - *

OOM protection still works: the body read is truncated at the limit. + * OOM protection still works: the body read is truncated at the limit. */ @Test public void testChunkedBodyExceedsLimit() throws Exception { @@ -252,8 +221,6 @@ public void testChunkedBodyExceedsLimit() throws Exception { body.contains("Error")); } - // -- Zero-limit behavior test ----------------------------------------------- - /** * When maxRequestSize is 0, SizeLimitHandler treats it as "reject all bodies > 0 bytes". * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} — 0 >= 0 is true, @@ -278,8 +245,6 @@ public void testZeroLimitRejectsAllBodies() throws Exception { } } - // -- checkBodySize vs SizeLimitHandler consistency tests -------------------- - /** * For pure ASCII JSON (the normal TRON API case), wire bytes and * {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures) @@ -320,7 +285,7 @@ public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { /** * When the body contains {@code \r\n} line endings, {@code lines().collect()} * normalizes them to {@code \n} (on Linux) or the platform line separator. - * This makes {@code checkBodySize} measure fewer bytes than the wire — + * This makes {@code checkBodySize} measure fewer bytes than the wire — * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. */ @Test @@ -338,8 +303,6 @@ public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception { wireBytes, servletBytes, wireBytes - servletBytes); } - // -- helpers ---------------------------------------------------------------- - /** POSTs with the given entity and returns the response body as a string. */ private String postForBody(URI uri, HttpEntity entity) throws Exception { HttpPost req = new HttpPost(uri); 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 ced7048c9d2..dc48972203a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -8,6 +8,8 @@ import com.google.gson.JsonObject; import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -25,6 +28,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.Metrics; import org.tron.common.utils.ByteArray; @@ -1099,4 +1103,74 @@ public void testWeb3ClientVersion() { Assert.fail(); } } + + /** + * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack. + * + * Covers: normal request no regression, Content-Length oversized 413, + * and chunked oversized handled gracefully (body truncated, 200 + empty body + * because RateLimiterServlet absorbs the BadMessageException). + */ + @Test + public void testJsonRpcSizeLimitIntegration() { + long testLimit = 1024; + try { + Field field = HttpService.class.getDeclaredField("maxRequestSize"); + field.setAccessible(true); + long originalLimit = field.getLong(fullNodeJsonRpcHttpService); + field.setLong(fullNodeJsonRpcHttpService, testLimit); + + fullNodeJsonRpcHttpService.start(); + String url = "http://127.0.0.1:" + + CommonParameter.getInstance().getJsonRpcHttpFullNodePort() + "/jsonrpc"; + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Normal JSON-RPC request passes through SizeLimitHandler + JsonObject req = new JsonObject(); + req.addProperty("jsonrpc", "2.0"); + req.addProperty("method", "web3_clientVersion"); + req.addProperty("id", 1); + + HttpPost post = new HttpPost(url); + post.addHeader("Content-Type", "application/json"); + post.setEntity(new StringEntity(req.toString())); + CloseableHttpResponse resp = httpClient.execute(post); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + String body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Normal JSON-RPC response should contain result", + body.contains("result")); + resp.close(); + + // Oversized request with Content-Length → 413 before JsonRpcServlet + HttpPost overPost = new HttpPost(url); + overPost.addHeader("Content-Type", "application/json"); + overPost.setEntity(new StringEntity( + new String(new char[(int) testLimit + 1]).replace('\0', 'x'))); + resp = httpClient.execute(overPost); + Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + resp.close(); + + // Chunked oversized → BadMessageException thrown during body read, + // absorbed by RateLimiterServlet catch(Exception) → 200 with empty body. + // Body read IS truncated at the limit — OOM protection effective. + byte[] chunkedData = new String(new char[(int) testLimit * 2]) + .replace('\0', 'x').getBytes("UTF-8"); + HttpPost chunkedPost = new HttpPost(url); + chunkedPost.setEntity(new InputStreamEntity( + new ByteArrayInputStream(chunkedData), -1)); + resp = httpClient.execute(chunkedPost); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Chunked oversized should return empty body" + + " (RateLimiterServlet absorbs BadMessageException)", body.isEmpty()); + resp.close(); + } finally { + field.setLong(fullNodeJsonRpcHttpService, originalLimit); + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } finally { + fullNodeJsonRpcHttpService.stop(); + } + } } From 58c258dcd2d001c11e72f95d9e577e8aada72122 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 16 Apr 2026 14:38:30 +0800 Subject: [PATCH 18/43] refactor(http): replace reflection with @VisibleForTesting accessor and fix comment attribution Add getMaxRequestSize()/setMaxRequestSize() to HttpService so tests use compile-safe accessors instead of Field.setAccessible(true). Correct comments attributing exception swallowing to RateLimiterServlet when it is actually jsonrpc4j that silently absorbs the BadMessageException. --- .../org/tron/common/application/HttpService.java | 11 +++++++++++ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 16 +++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 66a21a8a4ed..1318fd96527 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -15,6 +15,7 @@ package org.tron.common.application; +import com.google.common.annotations.VisibleForTesting; import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; @@ -32,6 +33,16 @@ public abstract class HttpService extends AbstractService { protected long maxRequestSize = 4 * 1024 * 1024; // 4MB + @VisibleForTesting + public long getMaxRequestSize() { + return this.maxRequestSize; + } + + @VisibleForTesting + public void setMaxRequestSize(long maxRequestSize) { + this.maxRequestSize = maxRequestSize; + } + @Override public void innerStart() throws Exception { if (this.apiServer != null) { 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 dc48972203a..1cf5178202a 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -9,7 +9,7 @@ import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; import java.io.ByteArrayInputStream; -import java.lang.reflect.Field; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -1109,16 +1109,14 @@ public void testWeb3ClientVersion() { * * Covers: normal request no regression, Content-Length oversized 413, * and chunked oversized handled gracefully (body truncated, 200 + empty body - * because RateLimiterServlet absorbs the BadMessageException). + * because jsonrpc4j absorbs the BadMessageException). */ @Test public void testJsonRpcSizeLimitIntegration() { long testLimit = 1024; try { - Field field = HttpService.class.getDeclaredField("maxRequestSize"); - field.setAccessible(true); - long originalLimit = field.getLong(fullNodeJsonRpcHttpService); - field.setLong(fullNodeJsonRpcHttpService, testLimit); + long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); + fullNodeJsonRpcHttpService.setMaxRequestSize(testLimit); fullNodeJsonRpcHttpService.start(); String url = "http://127.0.0.1:" @@ -1151,7 +1149,7 @@ public void testJsonRpcSizeLimitIntegration() { resp.close(); // Chunked oversized → BadMessageException thrown during body read, - // absorbed by RateLimiterServlet catch(Exception) → 200 with empty body. + // absorbed by jsonrpc4j catch(Exception) → 200 with empty body. // Body read IS truncated at the limit — OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); @@ -1162,10 +1160,10 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); body = EntityUtils.toString(resp.getEntity()); Assert.assertTrue("Chunked oversized should return empty body" - + " (RateLimiterServlet absorbs BadMessageException)", body.isEmpty()); + + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); resp.close(); } finally { - field.setLong(fullNodeJsonRpcHttpService, originalLimit); + fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); } } catch (Exception e) { Assert.fail(e.getMessage()); From f36075040107ca25c244cf558e10e9eeee795db6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 20 Apr 2026 10:05:07 +0800 Subject: [PATCH 19/43] test(http): address review comments - assertThrows, ASCII punctuation, finally guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace try/catch/fail pattern with Assert.assertThrows in UtilTest and ArgsTest (4 cases) - Replace non-ASCII punctuation (→, —) with ASCII (->, -) in newly added test comments - Hoist originalLimit before outer try in testJsonRpcSizeLimitIntegration so restore executes even when start() throws --- .../common/jetty/SizeLimitHandlerTest.java | 18 +++++----- .../org/tron/core/config/args/ArgsTest.java | 36 +++++++------------ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 13 ++++--- .../org/tron/core/services/http/UtilTest.java | 9 ++--- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index d3f60f9b119..685a861bc92 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -58,7 +58,7 @@ public class SizeLimitHandlerTest { /** * Simulates the real servlet pattern: reads body via getReader(), wraps in - * broad catch(Exception) — mirrors what RateLimiterServlet + actual servlets do. + * broad catch(Exception) - mirrors what RateLimiterServlet + actual servlets do. */ public static class BroadCatchServlet extends HttpServlet { @Override @@ -214,8 +214,8 @@ public void testChunkedBodyExceedsLimit() throws Exception { String body = EntityUtils.toString(resp.getEntity()); logger.info("Chunked oversized: status={}, body={}", status, body); - // catch(Exception) absorbs BadMessageException → 200 + error JSON, not 413. - // Body read IS truncated — OOM protection still effective. + // catch(Exception) absorbs BadMessageException -> 200 + error JSON, not 413. + // Body read IS truncated - OOM protection still effective. Assert.assertEquals(200, status); Assert.assertTrue("Error should be surfaced in response body", body.contains("Error")); @@ -223,8 +223,8 @@ public void testChunkedBodyExceedsLimit() throws Exception { /** * When maxRequestSize is 0, SizeLimitHandler treats it as "reject all bodies > 0 bytes". - * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} — 0 >= 0 is true, - * so any non-empty body triggers 413. This is NOT "pass all" — it is a silent DoS + * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} - 0 >= 0 is true, + * so any non-empty body triggers 413. This is NOT "pass all" - it is a silent DoS * against the node's own API. */ @Test @@ -248,7 +248,7 @@ public void testZeroLimitRejectsAllBodies() throws Exception { /** * For pure ASCII JSON (the normal TRON API case), wire bytes and * {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures) - * must be identical — the two enforcement layers agree exactly. + * must be identical - the two enforcement layers agree exactly. */ @Test public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { @@ -266,8 +266,8 @@ public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { /** * For UTF-8 JSON with multi-byte characters (CJK), wire bytes and - * {@code body.getBytes().length} must still be identical — UTF-8 round-trips - * through {@code request.getReader()} → {@code String.getBytes()} losslessly. + * {@code body.getBytes().length} must still be identical - UTF-8 round-trips + * through {@code request.getReader()} -> {@code String.getBytes()} losslessly. */ @Test public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { @@ -285,7 +285,7 @@ public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { /** * When the body contains {@code \r\n} line endings, {@code lines().collect()} * normalizes them to {@code \n} (on Linux) or the platform line separator. - * This makes {@code checkBodySize} measure fewer bytes than the wire — + * This makes {@code checkBodySize} measure fewer bytes than the wire - * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. */ @Test diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 63878f21740..6dc9f3d6ecb 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -472,12 +472,9 @@ public void testRpcMaxMessageSizeExceedsIntMax() { configMap.put("node.rpc.maxMessageSize", "3g"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected TronError for rpc maxMessageSize > Integer.MAX_VALUE"); - } catch (TronError e) { - Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); - } + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); } @Test @@ -487,12 +484,9 @@ public void testMaxMessageSizeNegativeValue() { configMap.put("node.rpc.maxMessageSize", "-4m"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected IllegalArgumentException for negative memory size"); - } catch (IllegalArgumentException e) { - Assert.assertTrue(e.getMessage().contains("negative")); - } + IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("negative")); } @Test @@ -502,12 +496,9 @@ public void testMaxMessageSizeInvalidUnit() { configMap.put("node.rpc.maxMessageSize", "4x"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected ConfigException.BadValue for invalid unit"); - } catch (ConfigException.BadValue e) { - Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); - } + ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); } @Test @@ -517,11 +508,8 @@ public void testMaxMessageSizeNonNumeric() { configMap.put("node.http.maxMessageSize", "abc"); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)); - try { - Args.applyConfigParams(config); - Assert.fail("expected ConfigException.BadValue for non-numeric value"); - } catch (ConfigException.BadValue e) { - Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); - } + ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); } } 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 1cf5178202a..cac7fc85714 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1114,8 +1114,8 @@ public void testWeb3ClientVersion() { @Test public void testJsonRpcSizeLimitIntegration() { long testLimit = 1024; + long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); try { - long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); fullNodeJsonRpcHttpService.setMaxRequestSize(testLimit); fullNodeJsonRpcHttpService.start(); @@ -1139,7 +1139,7 @@ public void testJsonRpcSizeLimitIntegration() { body.contains("result")); resp.close(); - // Oversized request with Content-Length → 413 before JsonRpcServlet + // Oversized request with Content-Length -> 413 before JsonRpcServlet HttpPost overPost = new HttpPost(url); overPost.addHeader("Content-Type", "application/json"); overPost.setEntity(new StringEntity( @@ -1148,9 +1148,9 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); resp.close(); - // Chunked oversized → BadMessageException thrown during body read, - // absorbed by jsonrpc4j catch(Exception) → 200 with empty body. - // Body read IS truncated at the limit — OOM protection effective. + // Chunked oversized -> BadMessageException thrown during body read, + // absorbed by jsonrpc4j catch(Exception) -> 200 with empty body. + // Body read IS truncated at the limit - OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); HttpPost chunkedPost = new HttpPost(url); @@ -1162,12 +1162,11 @@ public void testJsonRpcSizeLimitIntegration() { Assert.assertTrue("Chunked oversized should return empty body" + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); resp.close(); - } finally { - fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); } } catch (Exception e) { Assert.fail(e.getMessage()); } finally { + fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); fullNodeJsonRpcHttpService.stop(); } } diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 24a8a2bdef3..ebcb530bca3 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -143,12 +143,9 @@ public void testCheckBodySizeUsesHttpLimit() throws Exception { Util.checkBodySize(withinHttpLimit); String exceedsHttpLimit = new String(new char[201]).replace('\0', 'b'); - try { - Util.checkBodySize(exceedsHttpLimit); - Assert.fail("expected exception for body exceeding httpMaxMessageSize"); - } catch (Exception e) { - Assert.assertTrue(e.getMessage().contains("200")); - } + Exception e = Assert.assertThrows(Exception.class, + () -> Util.checkBodySize(exceedsHttpLimit)); + Assert.assertTrue(e.getMessage().contains("200")); } finally { Args.getInstance().setHttpMaxMessageSize(originalHttpMax); Args.getInstance().setMaxMessageSize(originalRpcMax); From bb14edd48adca18c883dbd04199854797a400d9c Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 22 Apr 2026 15:19:22 +0800 Subject: [PATCH 20/43] fix(config): cap http/jsonrpc maxMessageSize at Integer.MAX_VALUE Align node.http.maxMessageSize and node.jsonrpc.maxMessageSize with the existing node.rpc.maxMessageSize rule: reject values > Integer.MAX_VALUE at startup with TronError. Downstream body materialization is int-bounded (String/byte[]/StringBuilder all cap at Integer.MAX_VALUE), so config values beyond that produced an ambiguous silent mismatch; fail-fast is safer than a runtime NegativeArraySizeException or truncation surprise. Update config.conf docblocks (http/rpc/jsonrpc) to state the full constraint. Replace the obsolete 2Gi success assertion in testMaxMessageSizeHumanReadable, add testHttpMaxMessageSizeExceedsIntMax and testJsonRpcMaxMessageSizeExceedsIntMax mirroring the existing rpc case. --- .../java/org/tron/core/config/args/Args.java | 12 ++++-- framework/src/main/resources/config.conf | 9 +++-- .../org/tron/core/config/args/ArgsTest.java | 38 +++++++++++++------ 3 files changed, 40 insertions(+), 19 deletions(-) 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 ff20ed30241..3e181e118db 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 @@ -536,15 +536,19 @@ public static void applyConfigParams( PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() : defaultMaxMessageSize; - if (PARAMETER.httpMaxMessageSize < 0) { - throw new TronError("node.http.maxMessageSize must be non-negative, got: " + if (PARAMETER.httpMaxMessageSize < 0 + || PARAMETER.httpMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.http.maxMessageSize must be non-negative and <= " + + Integer.MAX_VALUE + ", got: " + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); } PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() : defaultMaxMessageSize; - if (PARAMETER.jsonRpcMaxMessageSize < 0) { - throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + if (PARAMETER.jsonRpcMaxMessageSize < 0 + || PARAMETER.jsonRpcMaxMessageSize > Integer.MAX_VALUE) { + throw new TronError("node.jsonrpc.maxMessageSize must be non-negative and <= " + + Integer.MAX_VALUE + ", got: " + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 8fc779546c3..e5ea11dd90e 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -225,7 +225,8 @@ node { PBFTPort = 8092 # The maximum request body size for HTTP API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m } @@ -254,7 +255,8 @@ node { # maxConnectionAgeInMillis = # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m @@ -365,7 +367,8 @@ node { jsonrpc { # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. Must be non-negative. + # Supports human-readable sizes: 4m, 4MB, 4194304. + # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). # Setting to 0 rejects all non-empty request bodies (not "unlimited"). # maxMessageSize = 4m diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 6dc9f3d6ecb..f13bf4c4d22 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -417,7 +417,7 @@ public void testMaxMessageSizeHumanReadable() { Args.clearParam(); // --- GB tier: binary (g/G/Gi/GiB) vs SI (GB) --- - // rpc is int-bounded, only test http/jsonrpc for GB values + // All three paths are int-bounded; values up to Integer.MAX_VALUE are accepted. configMap.put("node.rpc.maxMessageSize", "4m"); configMap.put("node.http.maxMessageSize", "1g"); configMap.put("node.jsonrpc.maxMessageSize", "1GB"); @@ -429,17 +429,6 @@ public void testMaxMessageSizeHumanReadable() { Assert.assertEquals(1000L * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); Args.clearParam(); - configMap.put("node.rpc.maxMessageSize", "4m"); - configMap.put("node.http.maxMessageSize", "2Gi"); - configMap.put("node.jsonrpc.maxMessageSize", "2GiB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)); - Args.applyConfigParams(config); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(2L * 1024 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - // --- raw integer (backward compatible): treated as bytes --- configMap.put("node.rpc.maxMessageSize", "4194304"); configMap.put("node.http.maxMessageSize", "4194304"); @@ -477,6 +466,31 @@ public void testRpcMaxMessageSizeExceedsIntMax() { Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); } + @Test + public void testHttpMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.http.maxMessageSize", "2Gi"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue(e.getMessage().contains("node.http.maxMessageSize must be non-negative")); + } + + @Test + public void testJsonRpcMaxMessageSizeExceedsIntMax() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.jsonrpc.maxMessageSize", "2Gi"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)); + TronError e = Assert.assertThrows(TronError.class, + () -> Args.applyConfigParams(config)); + Assert.assertTrue( + e.getMessage().contains("node.jsonrpc.maxMessageSize must be non-negative")); + } + @Test public void testMaxMessageSizeNegativeValue() { Map configMap = new HashMap<>(); From a4aaecaa4b9dd933f6e8d555a72bca11c654f6d9 Mon Sep 17 00:00:00 2001 From: GrapeS <156868965+Little-Peony@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:03:30 +0800 Subject: [PATCH 21/43] chore: fix outdated content across docs/ and root-level docs (#6662) * docs: update outdated content across all six doc files --- CONTRIBUTING.md | 6 +- build.md | 81 --------- docs/implement-a-customized-actuator-en.md | 69 ++++---- docs/implement-a-customized-actuator-zh.md | 69 ++++---- docs/modular-deployment-en.md | 13 +- docs/modular-deployment-zh.md | 13 +- docs/modular-introduction-en.md | 15 +- docs/modular-introduction-zh.md | 15 +- quickstart.md | 9 +- run.md | 193 --------------------- 10 files changed, 122 insertions(+), 361 deletions(-) delete mode 100644 build.md delete mode 100644 run.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a9dd75824..ef67a81e3ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,7 +147,7 @@ We would like all developers to follow a standard development flow and coding st 2. Review the code before submission. 3. Run standardized tests. -`Sonar`-scanner and `Travis CI` continuous integration scanner will be automatically triggered when a pull request has been submitted. When a PR passes all the checks, the **java-tron** maintainers will then review the PR and offer feedback and modifications when necessary. Once adopted, the PR will be closed and merged into the `develop` branch. +`Sonar`-scanner and CI checks (GitHub Actions) will be automatically triggered when a pull request has been submitted. When a PR passes all the checks, the **java-tron** maintainers will then review the PR and offer feedback and modifications when necessary. Once adopted, the PR will be closed and merged into the `develop` branch. We are glad to receive your pull requests and will try our best to review them as soon as we can. Any pull request is welcome, even if it is for a typo. @@ -161,7 +161,7 @@ Please make sure your submission meets the following code style: - The code must have passed the Sonar scanner test. - The code has to be pulled from the `develop` branch. - The commit message should start with a verb, whose initial should not be capitalized. -- The commit message should be less than 50 characters in length. +- The commit message title should be between 10 and 72 characters in length. @@ -196,7 +196,7 @@ The message header is a single line that contains succinct description of the ch The `scope` can be anything specifying place of the commit change. For example: `framework`, `api`, `tvm`, `db`, `net`. For a full list of scopes, see [Type and Scope Reference](#type-and-scope-reference). You can use `*` if there isn't a more fitting scope. The subject contains a succinct description of the change: -1. Limit the subject line, which briefly describes the purpose of the commit, to 50 characters. +1. Limit the subject line, which briefly describes the purpose of the commit, to 72 characters (minimum 10). 2. Start with a verb and use first-person present-tense (e.g., use "change" instead of "changed" or "changes"). 3. Do not capitalize the first letter. 4. Do not end the subject line with a period. diff --git a/build.md b/build.md deleted file mode 100644 index 1f3671b2c7d..00000000000 --- a/build.md +++ /dev/null @@ -1,81 +0,0 @@ -# How to Build - -## Prepare dependencies - -* JDK 1.8 (JDK 1.9+ are not supported yet) -* On Linux Ubuntu system (e.g. Ubuntu 16.04.4 LTS), ensure that the machine has [__Oracle JDK 8__](https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-get-on-ubuntu-16-04), instead of having __Open JDK 8__ in the system. If you are building the source code by using __Open JDK 8__, you will get [__Build Failed__](https://github.com/tronprotocol/java-tron/issues/337) result. -* Open **UDP** ports for connection to the network -* **Minimum** 2 CPU Cores - -## Build and Deploy automatically using scripts - -- Please take a look at the [Tron Deployment Scripts](https://github.com/tronprotocol/TronDeployment) repository. - -## Getting the code with git - -* Use Git from the console, see the [Setting up Git](https://help.github.com/articles/set-up-git/) and [Fork a Repo](https://help.github.com/articles/fork-a-repo/) articles. -* `develop` branch: the newest code -* `master` branch: more stable than develop. -In the shell command, type: - ```bash - git clone https://github.com/tronprotocol/java-tron.git - git checkout -t origin/master - ``` - -* For Mac, you can also install **[GitHub for Mac](https://mac.github.com/)** then **[fork and clone our repository](https://guides.github.com/activities/forking/)**. - -* If you'd rather not use Git, **[Download the ZIP](https://github.com/tronprotocol/java-tron/archive/develop.zip)** - -## Including java-tron as dependency - -If you don't want to checkout the code and build the project, you can include it directly as a dependency. - -**Using gradle:** - -``` -repositories { - maven { url 'https://jitpack.io' } -} -dependencies { - implementation 'com.github.tronprotocol:java-tron:develop-SNAPSHOT' -} -``` - -**Using maven:** - -```xml - - - jitpack.io - https://jitpack.io - - - - - com.github.tronprotocol - java-tron - develop-SNAPSHOT - - -``` - -## Building from source code - -- **Building using the console:** - - ```bash - cd java-tron - ./gradlew build - ``` - -- **Building using [IntelliJ IDEA](https://www.jetbrains.com/idea/) (community version is enough):** - - **Please run `./gradlew build` once to build the protocol files** - - 1. Start IntelliJ. - Select `File` -> `Open`, then locate to the java-tron folder which you have git cloned to your local drive. Then click `Open` button on the right bottom. - 2. Check on `Use auto-import` on the `Import Project from Gradle` dialog. Select JDK 1.8 in the `Gradle JVM` option. Then click `OK`. - 3. IntelliJ will import the project and start gradle syncing, which will take several minutes, depending on your network connection and your IntelliJ configuration - 4. Enable Annotations, `Preferences` -> Search `annotations` -> check `Enable Annotation Processing`. - 5. When the syncing finishes, select `Gradle` -> `Tasks` -> `build`, and then double click `build` option. - diff --git a/docs/implement-a-customized-actuator-en.md b/docs/implement-a-customized-actuator-en.md index 551a6d63d3b..9cd5b4958db 100644 --- a/docs/implement-a-customized-actuator-en.md +++ b/docs/implement-a-customized-actuator-en.md @@ -36,13 +36,19 @@ message Transaction { AccountCreateContract = 0; TransferContract = 1; ........ - SumContract = 52; + SumContract = 60; } ... } ``` -Then register a function to ensure that gRPC can receive and identify the requests of this contract. Currently, gRPC protocols are all defined in `src/main/protos/api/api.proto`. To add an `InvokeSum` interface in Wallet Service: +Then register a function to ensure that gRPC can receive and identify the requests of this contract. Currently, gRPC protocols are all defined in `src/main/protos/api/api.proto`. First add the import for the new proto file at the top of `api.proto`: + +```protobuf +import "core/contract/math_contract.proto"; +``` + +Then add an `InvokeSum` interface in the Wallet service: ```protobuf service Wallet { @@ -60,13 +66,11 @@ service Wallet { ``` At last, recompile the modified proto files. Compiling the java-tron project directly will compile the proto files as well, `protoc` command is also supported. -*Currently, java-tron uses protoc v3.4.0. Please keep the same version when compiling by `protoc` command.* - ```shell -# recommended +# recommended — also recompiles proto files automatically ./gradlew build -x test -# or build via protoc +# or build via protoc (ensure the protoc version matches the one declared in build.gradle) protoc -I=src/main/protos -I=src/main/protos/core --java_out=src/main/java Tron.proto protoc -I=src/main/protos/core/contract --java_out=src/main/java math_contract.proto protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java_out=src/main/java api.proto @@ -78,7 +82,11 @@ After compilation, the corresponding .class under the java_out directory will be For now, the default Actuator supported by java-tron is located in `org.tron.core.actuator`. Creating `SumActuator` under this directory: +> **Note**: The Actuator must be placed in the `org.tron.core.actuator` package. At node startup, `TransactionRegister.registerActuator()` uses reflection to scan that package and auto-discovers every `AbstractActuator` subclass. Each subclass is instantiated once (triggering the `super()` constructor which calls `TransactionFactory.register()`), so no manual registration code is needed. + ```java +import static org.tron.core.config.Parameter.ChainConstant.TRANSFER_FEE; + public class SumActuator extends AbstractActuator { public SumActuator() { @@ -210,48 +218,47 @@ At last, run a test class to validate whether the above steps are correct: ```java public class SumActuatorTest { private static final Logger logger = LoggerFactory.getLogger("Test"); - private String serviceNode = "127.0.0.1:50051"; - private String confFile = "config-localtest.conf"; - private String dbPath = "output-directory"; - private TronApplicationContext context; - private Application appTest; - private ManagedChannel channelFull = null; - private WalletGrpc.WalletBlockingStub blockingStubFull = null; + private static final String SERVICE_NODE = "127.0.0.1:50051"; + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); + + private static TronApplicationContext context; + private static Application appTest; + private static ManagedChannel channelFull; + private static WalletGrpc.WalletBlockingStub blockingStubFull; /** - * init the application. + * init the application once for all tests in this class. */ - @Before - public void init() { - CommonParameter argsTest = Args.getInstance(); - Args.setParam(new String[]{"--output-directory", dbPath}, - confFile); + @BeforeClass + public static void init() throws IOException { + Args.setParam(new String[]{"--output-directory", + temporaryFolder.newFolder().toString()}, "config-localtest.conf"); context = new TronApplicationContext(DefaultConfig.class); - RpcApiService rpcApiService = context.getBean(RpcApiService.class); appTest = ApplicationFactory.create(context); - appTest.addService(rpcApiService); - appTest.initServices(argsTest); - appTest.startServices(); appTest.startup(); - channelFull = ManagedChannelBuilder.forTarget(serviceNode) + channelFull = ManagedChannelBuilder.forTarget(SERVICE_NODE) .usePlaintext() .build(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); } /** - * destroy the context. + * destroy the context after all tests finish. */ - @After - public void destroy() throws InterruptedException { + @AfterClass + public static void destroy() throws InterruptedException { if (channelFull != null) { - channelFull.shutdown().awaitTermination(5, TimeUnit.SECONDS); + channelFull.shutdown(); + channelFull.awaitTermination(5, TimeUnit.SECONDS); } - Args.clearParam(); - appTest.shutdownServices(); appTest.shutdown(); context.destroy(); - FileUtil.deleteDir(new File(dbPath)); + Args.clearParam(); } @Test diff --git a/docs/implement-a-customized-actuator-zh.md b/docs/implement-a-customized-actuator-zh.md index a03f6aeb228..1c2bcd8f082 100644 --- a/docs/implement-a-customized-actuator-zh.md +++ b/docs/implement-a-customized-actuator-zh.md @@ -38,13 +38,19 @@ message Transaction { AccountCreateContract = 0; TransferContract = 1; ........ - SumContract = 52; + SumContract = 60; } ... } ``` -然后还需要注册一个方法来保证 gRPC 能够接收并识别该类型合约的请求,目前 gRPC 协议统一定义在 src/main/protos/api/api.proto,在 api.proto 中的 Wallet Service 新增 `InvokeSum` 接口: +然后还需要注册一个方法来保证 gRPC 能够接收并识别该类型合约的请求,目前 gRPC 协议统一定义在 src/main/protos/api/api.proto。首先在 `api.proto` 顶部添加对新 proto 文件的 import: + +```protobuf +import "core/contract/math_contract.proto"; +``` + +然后在 Wallet service 中新增 `InvokeSum` 接口: ```protobuf service Wallet { @@ -62,13 +68,11 @@ service Wallet { ``` 最后重新编译修改过 proto 文件,可自行编译也可直接通过编译 java-tron 项目来编译 proto 文件: -*目前 java-tron 采用的是 protoc v3.4.0,自行编译时确保 protoc 版本一致。* - ```shell -# recommended +# 推荐方式 —— 直接编译项目,proto 文件会自动重新编译 ./gradlew build -x test -# or build via protoc +# 或者手动使用 protoc(版本需与 build.gradle 中声明的一致) protoc -I=src/main/protos -I=src/main/protos/core --java_out=src/main/java Tron.proto protoc -I=src/main/protos/core/contract --java_out=src/main/java math_contract.proto protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java_out=src/main/java api.proto @@ -80,7 +84,11 @@ protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java 目前 java-tron 默认支持的 Actuator 存放在该模块的 org.tron.core.actuator 目录下,同样在该目录下创建 `SumActuator` : +> **注意**:Actuator 必须放在 `org.tron.core.actuator` 包下。节点启动时,`TransactionRegister.registerActuator()` 会通过反射扫描该包,自动发现所有 `AbstractActuator` 的子类,并各实例化一次(触发 `super()` 构造器,进而调用 `TransactionFactory.register()`)。因此无需手动编写注册代码。 + ```java +import static org.tron.core.config.Parameter.ChainConstant.TRANSFER_FEE; + public class SumActuator extends AbstractActuator { public SumActuator() { @@ -212,48 +220,47 @@ public class WalletApi extends WalletImplBase { ```java public class SumActuatorTest { private static final Logger logger = LoggerFactory.getLogger("Test"); - private String serviceNode = "127.0.0.1:50051"; - private String confFile = "config-localtest.conf"; - private String dbPath = "output-directory"; - private TronApplicationContext context; - private Application appTest; - private ManagedChannel channelFull = null; - private WalletGrpc.WalletBlockingStub blockingStubFull = null; + private static final String SERVICE_NODE = "127.0.0.1:50051"; + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); + + private static TronApplicationContext context; + private static Application appTest; + private static ManagedChannel channelFull; + private static WalletGrpc.WalletBlockingStub blockingStubFull; /** - * init the application. + * 整个测试类只初始化一次应用上下文。 */ - @Before - public void init() { - CommonParameter argsTest = Args.getInstance(); - Args.setParam(new String[]{"--output-directory", dbPath}, - confFile); + @BeforeClass + public static void init() throws IOException { + Args.setParam(new String[]{"--output-directory", + temporaryFolder.newFolder().toString()}, "config-localtest.conf"); context = new TronApplicationContext(DefaultConfig.class); - RpcApiService rpcApiService = context.getBean(RpcApiService.class); appTest = ApplicationFactory.create(context); - appTest.addService(rpcApiService); - appTest.initServices(argsTest); - appTest.startServices(); appTest.startup(); - channelFull = ManagedChannelBuilder.forTarget(serviceNode) + channelFull = ManagedChannelBuilder.forTarget(SERVICE_NODE) .usePlaintext() .build(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); } /** - * destroy the context. + * 所有测试结束后统一销毁上下文。 */ - @After - public void destroy() throws InterruptedException { + @AfterClass + public static void destroy() throws InterruptedException { if (channelFull != null) { - channelFull.shutdown().awaitTermination(5, TimeUnit.SECONDS); + channelFull.shutdown(); + channelFull.awaitTermination(5, TimeUnit.SECONDS); } - Args.clearParam(); - appTest.shutdownServices(); appTest.shutdown(); context.destroy(); - FileUtil.deleteDir(new File(dbPath)); + Args.clearParam(); } @Test diff --git a/docs/modular-deployment-en.md b/docs/modular-deployment-en.md index ef48f54b269..c93ba6c39d8 100644 --- a/docs/modular-deployment-en.md +++ b/docs/modular-deployment-en.md @@ -1,8 +1,8 @@ # How to deploy java-tron after modularization -After modularization, java-tron is launched via shell script instead of typing command: `java -jar FullNode.jar`. +After modularization, the recommended way to launch java-tron is via the shell script generated in `bin/`. The classic `java -jar FullNode.jar` command is still fully supported as an alternative. -*`java -jar FullNode.jar` still works, but will be deprecated in future*. +> **Supported platforms**: Linux and macOS. Windows is not supported. ## Download @@ -29,7 +29,7 @@ After unzip, two directories will be generated in java-tron: `bin` and `lib`, sh ## Startup -Use the corresponding script to start java-tron according to the OS type, use `*.bat` on Windows, Linux demo is as below: +Use the shell script to start java-tron (Linux / macOS): ``` # default java-tron-1.0.0/bin/FullNode @@ -45,12 +45,11 @@ java-tron-1.0.0/bin/FullNode -c config.conf -w JVM options can also be specified, located in `bin/java-tron.vmoptions`: ``` -# demo --XX:+UseConcMarkSweepGC +# demo (compatible with JDK 8 / JDK 17) +-Xms2g +-Xmx9g -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps --XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m --XX:+CMSScavengeBeforeRemark ``` \ No newline at end of file diff --git a/docs/modular-deployment-zh.md b/docs/modular-deployment-zh.md index 54a42df7d1f..27cc2ab3856 100644 --- a/docs/modular-deployment-zh.md +++ b/docs/modular-deployment-zh.md @@ -1,8 +1,8 @@ # 模块化后的 java-tron 部署方式 -模块化后,命令行下的程序启动方式将不再使用 `java -jar FullNode.jar` 的方式启动,而是使用脚本的方式启动,本文内容基于 develop 分支。 +模块化后,推荐使用 `bin/` 目录下生成的脚本启动 java-tron。原有的 `java -jar FullNode.jar` 方式仍完全支持,作为备选方式使用。 -*原有的启动方式依然保留,但即将废弃*。 +> **支持平台**:Linux 和 macOS。不支持 Windows。 ## 下载 @@ -29,7 +29,7 @@ unzip -o java-tron-1.0.0.zip ## 启动 -不同的 os 对应不同脚本,windows 即为 bat 文件,以 linux 系统为例启动 java-tron: +使用脚本启动 java-tron(Linux / macOS): ``` # 默认配置文件启动 java-tron-1.0.0/bin/FullNode @@ -43,12 +43,11 @@ java-tron-1.0.0/bin/FullNode -c config.conf -w java-tron 支持对 jvm 参数进行配置,配置文件为 bin 目录下的 java-tron.vmoptions 文件。 ``` -# demo --XX:+UseConcMarkSweepGC +# demo(兼容 JDK 8 / JDK 17) +-Xms2g +-Xmx9g -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps --XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m --XX:+CMSScavengeBeforeRemark ``` \ No newline at end of file diff --git a/docs/modular-introduction-en.md b/docs/modular-introduction-en.md index eab212e9771..654fbfcf995 100644 --- a/docs/modular-introduction-en.md +++ b/docs/modular-introduction-en.md @@ -16,7 +16,7 @@ The aim of java-tron modularization is to enable developers to easily build a de ![modular-structure](https://github.com/tronprotocol/java-tron/blob/develop/docs/images/module.png) -A modularized java-tron consists of six modules: framework, protocol, common, chainbase, consensus and actuator. The function of each module is elaborated below. +A modularized java-tron consists of nine modules: framework, protocol, common, chainbase, consensus, actuator, crypto, plugins and platform. The function of each module is elaborated below. ### framework @@ -67,4 +67,15 @@ Actuator module defines the `Actuator` interface, which includes 4 different met 4. calcFee: define the logic of calculating transaction fees Depending on their businesses, developers may set up Actuator accordingly and customize the processing of different types of transactions. - \ No newline at end of file + +### crypto + +Crypto module encapsulates cryptographic primitives used across the project, including elliptic curve key operations, hash functions and signature verification. It depends only on `common` and has no dependency on other business modules, keeping cryptographic logic isolated and auditable. + +### plugins + +Plugins module provides standalone operational tools packaged as independent executable JARs, such as `Toolkit.jar` and `ArchiveManifest.jar`. These tools support database maintenance tasks like migration, compaction and lite-node data pruning, and can be run without starting a full node. + +### platform + +Platform module provides the JNI bindings for the native database engines — LevelDB and RocksDB. It is architecture-aware: LevelDB is excluded on ARM64 (Apple Silicon and Linux aarch64) where only RocksDB is supported, while both are available on x86_64. diff --git a/docs/modular-introduction-zh.md b/docs/modular-introduction-zh.md index ba2c5d4b8f5..e1a02f6b778 100644 --- a/docs/modular-introduction-zh.md +++ b/docs/modular-introduction-zh.md @@ -14,7 +14,7 @@ java-tron 模块化的目的是为了帮助开发者方便地构建出特定应 ![modular-structure](https://github.com/tronprotocol/java-tron/blob/develop/docs/images/module.png) -模块化后的 java-tron 目前分为6个模块:framework、protocol、common、chainbase、consensus、actuator,下面分别简单介绍一下各个模块的作用。 +模块化后的 java-tron 目前分为9个模块:framework、protocol、common、chainbase、consensus、actuator、crypto、plugins、platform,下面分别简单介绍一下各个模块的作用。 ### framework @@ -65,4 +65,15 @@ actuator模块定义了 Actuator 接口,该接口有4个方法: 4. calcFee: 定义交易手续费计算逻辑 开发者可以根据自身业务实现 Actuator 接口,就能实现自定义交易类型的处理。 - \ No newline at end of file + +### crypto + +crypto 模块封装了项目中使用的密码学原语,包括椭圆曲线密钥操作、哈希函数及签名验证等。该模块仅依赖 `common`,不依赖其他业务模块,保持密码学逻辑的独立性与可审计性。 + +### plugins + +plugins 模块提供独立的运维工具,打包为可单独执行的 JAR(如 `Toolkit.jar`、`ArchiveManifest.jar`)。这些工具支持数据库迁移、压缩、轻节点数据裁剪等维护任务,无需启动完整节点即可运行。 + +### platform + +platform 模块提供原生数据库引擎 LevelDB 和 RocksDB 的 JNI 绑定,与架构强相关:ARM64(Apple Silicon 及 Linux aarch64)平台仅支持 RocksDB,LevelDB 被排除;x86_64 平台两者均可使用。 diff --git a/quickstart.md b/quickstart.md index 6eda855f1e9..b3eeb7b7713 100644 --- a/quickstart.md +++ b/quickstart.md @@ -45,7 +45,7 @@ docker pull tronprotocol/java-tron You can run the command below to start the java-tron: ``` -docker run -it -d -p 8090:8090 -p 8091:8091 -p 18888:18888 -p 50051:50051 --restart always tronprotocol/java-tron +docker run -it -d -p 8090:8090 -p 18888:18888 -p 50051:50051 --restart always tronprotocol/java-tron ``` The `-p` flag defines the ports that the container needs to be mapped on the host machine. By default the container will start and join in the mainnet @@ -65,8 +65,9 @@ Note: The directory `/Users/tron/docker/conf` must contain the file `config-loca ## Quickstart for using docker-tron-quickstart -The image exposes a Full Node, Solidity Node, and Event Server. Through TRON Quickstart, users can deploy DApps, smart contracts, and interact with the TronWeb library. -Check more information at [Quickstart:](https://github.com/TRON-US/docker-tron-quickstart) +The image exposes a Full Node and Event Server. Through TRON Quickstart, users can deploy DApps, smart contracts, and interact with the TronWeb library. + +> Note: `docker-tron-quickstart` is a community-maintained tool. Check its repository for the latest status: [Quickstart](https://github.com/TRON-US/docker-tron-quickstart) ### Node.JS Console Node.JS is used to interact with the Full and Solidity Nodes via Tron-Web. @@ -84,7 +85,7 @@ docker pull trontools/quickstart ## Setup TRON Quickstart ### TRON Quickstart Run -Run the "docker run" command to launch TRON Quickstart. TRON Quickstart exposes port 9090 for Full Node, Solidity Node, and Event Server. +Run the "docker run" command to launch TRON Quickstart. TRON Quickstart exposes port 9090 for Full Node and Event Server. ```shell docker run -it \ -p 9090:9090 \ diff --git a/run.md b/run.md deleted file mode 100644 index c0ecbe4d91f..00000000000 --- a/run.md +++ /dev/null @@ -1,193 +0,0 @@ -# How to Running - -### Running multi-nodes - -https://github.com/tronprotocol/Documentation/blob/master/TRX/Solidity_and_Full_Node_Deployment_EN.md - -## Running a local node and connecting to the public testnet - -Use the [Testnet Config](https://github.com/tronprotocol/TronDeployment/blob/master/test_net_config.conf) or use the [Tron Deployment Scripts](https://github.com/tronprotocol/TronDeployment). - - -### Running a Super Representative Node for mainnet - -**Use the executable JAR(Recommended way):** - -```bash -java -jar FullNode.jar -p --witness -c your config.conf(Example:/data/java-tron/config.conf) -Example: -java -jar FullNode.jar -p --witness -c /data/java-tron/config.conf - -``` - -This is similar to running a private testnet, except that the IPs in the `config.conf` are officially declared by TRON. - -

-Correct output - -```bash - -20:43:18.138 INFO [main] [o.t.p.FullNode](FullNode.java:21) Full node running. -20:43:18.486 INFO [main] [o.t.c.c.a.Args](Args.java:429) Bind address wasn't set, Punching to identify it... -20:43:18.493 INFO [main] [o.t.c.c.a.Args](Args.java:433) UDP local bound to: 10.0.8.146 -20:43:18.495 INFO [main] [o.t.c.c.a.Args](Args.java:448) External IP wasn't set, using checkip.amazonaws.com to identify it... -20:43:19.450 INFO [main] [o.t.c.c.a.Args](Args.java:461) External address identified: 47.74.147.87 -20:43:19.599 INFO [main] [o.s.c.a.AnnotationConfigApplicationContext](AbstractApplicationContext.java:573) Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@124c278f: startup date [Fri Apr 27 20:43:19 CST 2018]; root of context hierarchy -20:43:19.972 INFO [main] [o.s.b.f.a.AutowiredAnnotationBeanPostProcessor](AutowiredAnnotationBeanPostProcessor.java:153) JSR-330 'javax.inject.Inject' annotation found and supported for autowiring -20:43:20.380 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:244) update latest block header timestamp = 0 -20:43:20.383 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:252) update latest block header number = 0 -20:43:20.393 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:260) update latest block header id = 00 -20:43:20.394 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:265) update state flag = 0 -20:43:20.559 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.567 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.568 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.568 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.569 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.596 INFO [main] [o.t.c.d.Manager](Manager.java:300) create genesis block -20:43:20.607 INFO [main] [o.t.c.d.Manager](Manager.java:306) save block: BlockCapsule - -``` - -Then observe whether block synchronization success,If synchronization successfully explains the success of the super node - -
- - -### Running a Super Representative Node for private testnet -* use master branch -* You should modify the config.conf - 1. Replace existing entry in genesis.block.witnesses with your address. - 2. Replace existing entry in seed.node ip.list with your ip list. - 3. The first Super Node start, needSyncCheck should be set false - 4. Set p2pversion to 61 - -* Use the executable JAR(Recommended way) - -```bash -cd build/libs -java -jar FullNode.jar -p --witness -c your config.conf (Example:/data/java-tron/config.conf) -Example: -java -jar FullNode.jar -p --witness -c /data/java-tron/config.conf - -``` - -
-Show Output - -```bash -> ./gradlew run -Pwitness - -> Task :generateProto UP-TO-DATE -Using TaskInputs.file() with something that doesn't resolve to a File object has been deprecated and is scheduled to be removed in Gradle 5.0. Use TaskInputs.files() instead. - -> Task :run -20:39:22.749 INFO [o.t.c.c.a.Args] private.key = 63e62a71ed3... -20:39:22.816 WARN [o.t.c.c.a.Args] localwitness size must be one, get the first one -20:39:22.832 INFO [o.t.p.FullNode] Here is the help message.output-directory/ -三月 22, 2018 8:39:23 下午 org.tron.core.services.RpcApiService start -信息: Server started, listening on 50051 -20:39:23.706 INFO [o.t.c.o.n.GossipLocalNode] listener message -20:39:23.712 INFO [o.t.c.o.n.GossipLocalNode] sync group = a41d27f10194c53703be90c6f8735bb66ffc53aa10ea9024d92dbe7324b1aee3 -20:39:23.716 INFO [o.t.c.s.WitnessService] Sleep : 1296 ms,next time:2018-03-22T20:39:25.000+08:00 -20:39:23.734 WARN [i.s.t.BootstrapFactory] Env doesn't support epoll transport -20:39:23.746 INFO [i.s.t.TransportImpl] Bound to: 192.168.10.163:7080 -20:39:23.803 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:25.019 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:25.067+08:00] -20:39:25.019 INFO [o.t.c.s.WitnessService] ScheduledWitness[448d53b2df0cd78158f6f0aecdf60c1c10b15413],slot[1946] -20:39:25.021 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:25.021 INFO [o.t.c.s.WitnessService] Sleep : 4979 ms,next time:2018-03-22T20:39:30.000+08:00 -20:39:30.003 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:30.052+08:00] -20:39:30.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf],slot[1947] -20:39:30.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:30.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:35.000+08:00 -20:39:33.803 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:35.005 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:35.054+08:00] -20:39:35.005 INFO [o.t.c.s.WitnessService] ScheduledWitness[48e447ec869216de76cfeeadf0db37a3d1c8246d],slot[1948] -20:39:35.005 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:35.005 INFO [o.t.c.s.WitnessService] Sleep : 4995 ms,next time:2018-03-22T20:39:40.000+08:00 -20:39:40.005 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:40.055+08:00] -20:39:40.010 INFO [o.t.c.d.Manager] postponedTrxCount[0],TrxLeft[0] -20:39:40.022 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header id = fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2 -20:39:40.022 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 104, 97, 115, 104], BytesCapsule is org.tron.core.capsule.BytesCapsule@2ce0e954 -20:39:40.023 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header number = 140 -20:39:40.024 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 110, 117, 109, 98, 101, 114], BytesCapsule is org.tron.core.capsule.BytesCapsule@83924ab -20:39:40.024 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header timestamp = 1521722380001 -20:39:40.024 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 116, 105, 109, 101, 115, 116, 97, 109, 112], BytesCapsule is org.tron.core.capsule.BytesCapsule@ca6a6f8 -20:39:40.024 INFO [o.t.c.d.Manager] updateWitnessSchedule number:140,HeadBlockTimeStamp:1521722380001 -20:39:40.025 WARN [o.t.c.u.RandomGenerator] index[-3] is out of range[0,3],skip -20:39:40.070 INFO [o.t.c.d.TronStoreWithRevoking] Address is [73, 72, -62, -24, -89, 86, -39, 67, 112, 55, -36, -40, -57, -32, -57, 61, 86, 12, -93, -115], AccountCapsule is account_name: "Sun" -address: "IH\302\350\247V\331Cp7\334\330\307\340\307=V\f\243\215" -balance: 9223372036854775387 - -20:39:40.081 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], AccountCapsule is type: AssetIssue -address: ")\237=\270\n$\262\n%K\211\316c\235Y\023/\025\177\023" -balance: 420 - -20:39:40.082 INFO [o.t.c.d.TronStoreWithRevoking] Address is [76, 65, 84, 69, 83, 84, 95, 83, 79, 76, 73, 68, 73, 70, 73, 69, 68, 95, 66, 76, 79, 67, 75, 95, 78, 85, 77], BytesCapsule is org.tron.core.capsule.BytesCapsule@ec1439 -20:39:40.083 INFO [o.t.c.d.Manager] there is account List size is 8 -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 448d53b2df0cd78158f6f0aecdf60c1c10b15413 -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 548794500882809695a8a687866e76d4271a146a -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 48e447ec869216de76cfeeadf0db37a3d1c8246d -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 55ddae14564f82d5b94c7a131b5fcfd31ad6515a -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 299f3db80a24b20a254b89ce639d59132f157f13 -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is abd4b9367799eaa3197fecb144eb71de1e049150 -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 4948c2e8a756d9437037dcd8c7e0c73d560ca38d -20:39:40.085 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 34, -63, -81, 123, -5, -78, -80, -32, 113, 72, -20, -70, 39, -75, 111, -127, -91, 79, -49], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@4cb4f7fb -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@7be2474a -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [72, -28, 71, -20, -122, -110, 22, -34, 118, -49, -18, -83, -16, -37, 55, -93, -47, -56, 36, 109], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@3e375891 -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [68, -115, 83, -78, -33, 12, -41, -127, 88, -10, -16, -82, -51, -10, 12, 28, 16, -79, 84, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@55d77b83 -20:39:40.090 INFO [o.t.c.d.Manager] countWitnessMap size is 0 -20:39:40.091 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@310dd876 -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [72, -28, 71, -20, -122, -110, 22, -34, 118, -49, -18, -83, -16, -37, 55, -93, -47, -56, 36, 109], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@151b42bc -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 34, -63, -81, 123, -5, -78, -80, -32, 113, 72, -20, -70, 39, -75, 111, -127, -91, 79, -49], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@2d0388aa -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [68, -115, 83, -78, -33, 12, -41, -127, 88, -10, -16, -82, -51, -10, 12, 28, 16, -79, 84, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@478a55e7 -20:39:40.101 INFO [o.t.c.d.TronStoreWithRevoking] Address is [-3, 48, -95, 97, 96, 113, 95, 60, -95, -91, -68, -83, 24, -24, 25, -111, -51, 111, 71, 38, 90, 113, -127, 91, -46, -55, 67, 18, -101, 37, -116, -46], BlockCapsule is BlockCapsule{blockId=fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2, num=140, parentId=dadeff07c32d342b941cfa97ba82870958615e7ae73fffeaf3c6a334d81fe3bd, generatedByMyself=true} -20:39:40.102 INFO [o.t.c.d.Manager] save block: BlockCapsule{blockId=fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2, num=140, parentId=dadeff07c32d342b941cfa97ba82870958615e7ae73fffeaf3c6a334d81fe3bd, generatedByMyself=true} -20:39:40.102 INFO [o.t.c.s.WitnessService] Block is generated successfully, Its Id is fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2,number140 -20:39:40.102 INFO [o.t.c.n.n.NodeImpl] Ready to broadcast a block, Its hash is fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2 -20:39:40.107 INFO [o.t.c.s.WitnessService] Produced -20:39:40.107 INFO [o.t.c.s.WitnessService] Sleep : 4893 ms,next time:2018-03-22T20:39:45.000+08:00 -20:39:43.805 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:45.002 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T20:39:45.001+08:00],now[2018-03-22T20:39:45.052+08:00] -20:39:45.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[48e447ec869216de76cfeeadf0db37a3d1c8246d],slot[1] -20:39:45.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:45.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:50.000+08:00 -20:39:50.002 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T20:39:45.001+08:00],now[2018-03-22T20:39:50.052+08:00] -20:39:50.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf],slot[2] -20:39:50.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:50.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:55.000+08:00 - -``` - -
- -* In IntelliJ IDEA - -
- - -Open the configuration panel: - - - -![](docs/images/program_configure.png) - -
- -
- - -In the `Program arguments` option, fill in `--witness`: - - - -![](docs/images/set_witness_param.jpeg) - -
- -Then, run `FullNode::main()` again. - -## Advanced Configurations - -Read the [Advanced Configurations](common/src/main/java/org/tron/core/config/README.md). From ba2b77ff816c4d360f9326aaeca1262fcca7f53d Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:51:53 +0800 Subject: [PATCH 22/43] fix(trie): make TrieImpl.insert() idempotent for duplicate key-value (#6696) * fix(trie): make TrieImpl.insert() idempotent for duplicate key-value puts Reorder condition checks in insert() so that commonPrefix.equals(k) is evaluated before commonPrefix.isEmpty(). When both k and currentNodeKey are empty (which happens on a duplicate put of a fully-split key), the old order incorrectly fired the "no common prefix" branch and replaced KVNode("",v) with BranchNode{terminal:v}, corrupting the root hash. Also short-circuit kvNodeSetValueOrNode() when the new value equals the existing one (by reference or by byte content) to avoid unnecessary dirty marking and downstream hash recomputation. close #6608 * test(trie): strengthen testOrder invariant and clean up * feat(trie): add explain for #6608 --- .../java/org/tron/core/trie/TrieImpl.java | 15 +- .../java/org/tron/core/tire/TrieTest.java | 138 ++++++++++++++---- 2 files changed, 118 insertions(+), 35 deletions(-) diff --git a/framework/src/main/java/org/tron/core/trie/TrieImpl.java b/framework/src/main/java/org/tron/core/trie/TrieImpl.java index b256cbe323d..586c3b2b893 100644 --- a/framework/src/main/java/org/tron/core/trie/TrieImpl.java +++ b/framework/src/main/java/org/tron/core/trie/TrieImpl.java @@ -179,14 +179,18 @@ private Node insert(Node n, TrieKey k, Object nodeOrValue) { } else { TrieKey currentNodeKey = n.kvNodeGetKey(); TrieKey commonPrefix = k.getCommonPrefix(currentNodeKey); - if (commonPrefix.isEmpty()) { + // NOTE: equals(k) MUST precede isEmpty(). They overlap only when both k and + // currentNodeKey are empty (duplicate put on a fully-split KV leaf); in that + // case the correct behavior is an in-place value update, not conversion to + // a BranchNode. Swapping the order corrupts the trie structure. See #6608. + if (commonPrefix.equals(k)) { + return n.kvNodeSetValueOrNode(nodeOrValue); + } else if (commonPrefix.isEmpty()) { Node newBranchNode = new Node(); insert(newBranchNode, currentNodeKey, n.kvNodeGetValueOrNode()); insert(newBranchNode, k, nodeOrValue); n.dispose(); return newBranchNode; - } else if (commonPrefix.equals(k)) { - return n.kvNodeSetValueOrNode(nodeOrValue); } else if (commonPrefix.equals(currentNodeKey)) { insert(n.kvNodeGetChildNode(), k.shift(commonPrefix.getLength()), nodeOrValue); return n.invalidate(); @@ -873,6 +877,11 @@ public Object kvNodeGetValueOrNode() { public Node kvNodeSetValueOrNode(Object valueOrNode) { parse(); assert getType() != NodeType.BranchNode; + if (valueOrNode instanceof byte[] && children[1] instanceof byte[] + && (children[1] == valueOrNode + || Arrays.equals((byte[]) children[1], (byte[]) valueOrNode))) { + return this; + } children[1] = valueOrNode; dirty = true; return this; diff --git a/framework/src/test/java/org/tron/core/tire/TrieTest.java b/framework/src/test/java/org/tron/core/tire/TrieTest.java index 82f7e26dc57..a12472a8a34 100644 --- a/framework/src/test/java/org/tron/core/tire/TrieTest.java +++ b/framework/src/test/java/org/tron/core/tire/TrieTest.java @@ -19,13 +19,13 @@ package org.tron.core.tire; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.bouncycastle.util.Arrays; +import java.util.Random; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.tron.core.capsule.utils.FastByteComparisons; import org.tron.core.capsule.utils.RLP; @@ -41,12 +41,13 @@ public class TrieTest { private static String doge = "doge"; private static String test = "test"; private static String dude = "dude"; + private static final long SHUFFLE_SEED = 0xC0FFEEL; @Test public void test() { TrieImpl trie = new TrieImpl(); trie.put(new byte[]{1}, c.getBytes()); - Assert.assertTrue(Arrays.areEqual(trie.get(RLP.encodeInt(1)), c.getBytes())); + Assert.assertArrayEquals(trie.get(RLP.encodeInt(1)), c.getBytes()); trie.put(new byte[]{1, 0}, ca.getBytes()); trie.put(new byte[]{1, 1}, cat.getBytes()); trie.put(new byte[]{1, 2}, dog.getBytes()); @@ -64,8 +65,6 @@ public void test() { boolean result = trie .verifyProof(trieCopy.getRootHash(), new byte[]{1, 1}, (LinkedHashMap) map); Assert.assertTrue(result); - System.out.println(trieCopy.prove(RLP.encodeInt(5))); - System.out.println(trieCopy.prove(RLP.encodeInt(6))); assertTrue(RLP.encodeInt(5), trieCopy); assertTrue(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); assertTrue(RLP.encodeInt(6), trieCopy); @@ -73,13 +72,13 @@ public void test() { // trie.put(RLP.encodeInt(5), doge.getBytes()); byte[] rootHash2 = trie.getRootHash(); - Assert.assertFalse(Arrays.areEqual(rootHash, rootHash2)); + Assert.assertArrayEquals(rootHash, rootHash2); trieCopy = new TrieImpl(trie.getCache(), rootHash2); // assertTrue(RLP.encodeInt(5), trieCopy); - assertFalse(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); + assertTrue(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); assertTrue(RLP.encodeInt(6), trieCopy); - assertFalse(RLP.encodeInt(6), RLP.encodeInt(5), trieCopy); + assertTrue(RLP.encodeInt(6), RLP.encodeInt(5), trieCopy); } @Test @@ -96,7 +95,7 @@ public void test1() { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); } byte[] rootHash2 = trie2.getRootHash(); - Assert.assertTrue(Arrays.areEqual(rootHash1, rootHash2)); + Assert.assertArrayEquals(rootHash1, rootHash2); } @Test @@ -121,29 +120,18 @@ public void test2() { } /* - * Known TrieImpl bug: insert() is not idempotent for duplicate key-value pairs. - * - * This test inserts keys 1-99, then re-inserts key 10 with the same value, - * shuffles the order, and expects the root hash to be identical. It fails - * intermittently (~3% of runs) because: - * - * 1. The value list contains key 10 twice (line value.add(10)). - * 2. Collections.shuffle() randomizes the position of both 10s. - * 3. TrieImpl.insert() calls kvNodeSetValueOrNode() + invalidate() even when - * the value hasn't changed, corrupting internal node cache state. - * 4. Subsequent insertions between the two put(10) calls cause different - * tree split/merge paths depending on the shuffle order. - * 5. The final root hash becomes insertion-order-dependent, violating the - * Merkle Trie invariant. + * Verifies that TrieImpl root hash is insertion-order-independent even when + * the same key is put more than once (idempotent put). * - * Production impact: low. AccountStateCallBack uses TrieImpl to build per-block - * account state tries. Duplicate put(key, sameValue) is unlikely in production - * because account state changes between transactions (balance, nonce, etc.). - * - * Proper fix: TrieImpl.insert() should short-circuit when the existing value - * equals the new value, avoiding unnecessary invalidate(). See TrieImpl:188-192. + * Covers both known-failing sequences (regression) and a seeded random + * shuffle. Previously flaky due to a correctness bug in TrieImpl.insert(): + * commonPrefix.isEmpty() was checked before commonPrefix.equals(k), causing + * KVNode("", v_old) to be incorrectly replaced with BranchNode{terminal:v_new} + * on a duplicate put of a fully-split key — this is the actual root-hash + * corruption. A separate, non-correctness optimization in + * kvNodeSetValueOrNode() additionally short-circuits same-value writes to + * avoid unnecessary dirty marking / hash recomputation. */ - @Ignore("TrieImpl bug: root hash depends on insertion order with duplicate key-value puts") @Test public void testOrder() { TrieImpl trie = new TrieImpl(); @@ -156,7 +144,73 @@ public void testOrder() { trie.put(RLP.encodeInt(10), String.valueOf(10).getBytes()); value.add(10); byte[] rootHash1 = trie.getRootHash(); - Collections.shuffle(value); + TrieImpl baseline = new TrieImpl(); + for (int i = 1; i < n; i++) { + baseline.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); + } + Assert.assertArrayEquals(baseline.getRootHash(), rootHash1); + Collections.shuffle(value, new Random(SHUFFLE_SEED)); + assertTrieRootHash(rootHash1, value); + String[] sequences = { + "95,10,66,10,67,2,98,31,85,89,81,96,19,68,44,49,43,40,62,87,4,38,17,18,8," + + "74,28,51,3,41,99,80,70,61,26,34,86,15,33,52,25,92,77,11,39,88,46,84,7,48," + + "82,91,16,56,90,65,30,53,47,14,32,79,1,42,45,29,13,22,5,23,59,97,12,20,37," + + "54,64,57,78,6,27,50,58,93,83,76,94,72,69,60,75,55,35,63,21,71,24,73,36,9", + "42,10,78,80,37,10,55,20,58,8,47,84,52,22,27,79,19,34,3,69,49,74,97,81,39," + + "4,48,11,68,30,60,98,73,33,86,36,67,94,92,43,88,23,40,28,18,46,50,45,21,14," + + "26,24,66,32,71,91,5,95,59,51,38,29,12,41,75,89,16,15,87,85,77,17,96,63,7," + + "57,54,35,61,83,31,2,72,90,53,9,44,56,6,1,70,64,25,82,62,99,13,93,76,65", + "74,83,94,10,28,91,10,29,20,58,2,5,36,41,12,27,19,48,80,38,33,15,46,32,64," + + "13,95,1,7,42,26,90,31,77,34,60,56,44,17,23,52,39,87,35,22,37,14,67,86,4," + + "93,68,45,71,97,18,98,73,75,53,51,57,72,9,96,78,40,66,92,30,81,50,6,59,61," + + "8,65,76,69,16,11,88,25,89,3,54,49,43,62,24,21,82,70,47,84,55,79,99,63,85", + "99,35,66,10,78,29,70,46,75,10,23,61,60,7,25,20,31,37,52,77,80,11,34,89,65," + + "88,28,64,43,81,92,87,72,40,38,67,54,26,73,15,8,90,63,21,49,1,85,17,74,97," + + "91,16,36,6,2,56,94,3,62,95,32,58,39,51,14,59,27,96,83,50,86,84,48,19,24," + + "82,5,41,13,33,18,44,79,42,68,4,57,45,76,55,9,69,93,12,53,98,22,30,47,71", + "27,47,18,78,87,10,98,20,45,33,10,46,56,5,24,39,11,40,14,73,66,76,96,44,42," + + "53,69,50,61,29,94,55,35,72,99,43,57,91,85,9,48,86,32,92,64,97,67,75,7,58," + + "34,4,88,63,70,80,83,82,22,30,84,60,36,54,62,28,21,38,51,25,81,41,52,15," + + "77,93,89,13,95,3,49,31,17,59,26,2,23,12,71,16,90,79,68,6,1,37,74,65,19,8", + "80,60,17,71,92,47,52,10,61,10,97,44,57,45,86,55,96,34,27,77,50,91,32,24,8," + + "67,33,94,19,5,4,37,70,63,13,68,69,85,29,49,23,76,40,81,99,15,73,41,12,83," + + "93,64,1,79,58,89,88,21,53,6,39,95,74,22,9,78,46,18,11,54,30,90,31,98,36," + + "38,75,48,25,72,28,14,66,26,56,3,16,43,62,82,59,87,84,35,2,7,20,42,51,65", + "94,73,70,10,36,10,50,54,89,37,20,95,82,47,6,32,12,39,80,65,41,44,13,86,27," + + "66,49,30,58,51,21,59,56,16,5,38,81,90,67,11,35,55,14,97,79,29,75,57,24," + + "43,92,78,71,93,85,72,18,52,28,87,31,83,9,99,46,17,25,42,96,15,8,22,45,76," + + "77,7,91,53,1,4,3,84,62,40,60,61,19,98,63,2,88,26,68,33,64,23,34,74,69,48", + "64,73,78,46,10,37,10,20,19,94,56,57,69,31,82,54,96,4,87,59,30,84,9,23,76," + + "2,72,36,71,40,24,49,44,95,98,16,35,45,77,67,80,33,32,29,91,53,39,14,52," + + "81,13,25,90,79,28,61,26,83,62,41,34,43,86,66,50,58,21,22,7,38,74,42,48," + + "93,55,68,51,89,12,88,60,6,92,99,18,65,15,8,63,17,1,85,70,75,3,27,97,11," + + "47,5", + "10,78,26,27,10,56,24,38,70,23,48,21,77,97,83,20,67,74,29,36,15,16,6,19,90," + + "88,1,13,93,25,11,79,52,61,84,40,99,12,81,98,2,58,54,66,7,9,31,30,60,47," + + "63,75,44,34,86,37,57,76,5,72,94,14,95,55,51,18,82,3,89,46,33,69,59,96," + + "17,41,92,53,87,71,8,80,28,73,85,39,32,45,4,22,35,43,65,62,50,49,91,64," + + "68,42", + "10,10,97,52,89,91,66,28,59,60,58,76,17,67,44,79,88,7,48,50,61,70,39,75,95," + + "69,38,55,98,37,25,84,49,35,85,72,29,83,74,99,21,53,32,81,73,16,19,6,92," + + "12,96,46,40,14,47,15,27,36,78,82,3,2,8,26,20,33,57,63,65,77,54,1,64,34," + + "5,4,18,13,30,9,43,93,90,80,62,11,42,45,51,41,86,94,24,71,22,56,23,31," + + "87,68" + }; + for (String sequence : sequences) { + assertTrieRootHash(rootHash1, parseSeq(sequence)); + } + } + + private static List parseSeq(String csv) { + String[] parts = csv.split(","); + List result = new ArrayList<>(parts.length); + for (String p : parts) { + result.add(Integer.parseInt(p)); + } + return result; + } + + private static void assertTrieRootHash(byte[] rootHash1, List value) { TrieImpl trie2 = new TrieImpl(); for (int i : value) { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); @@ -165,6 +219,26 @@ public void testOrder() { Assert.assertArrayEquals(rootHash1, rootHash2); } + @Test + public void testDeleteDirtyPropagation() { + TrieImpl trie = new TrieImpl(); + byte[] key1 = new byte[]{0x01, 0x00}; + byte[] key2 = new byte[]{0x01, 0x01}; + byte[] key3 = new byte[]{0x01, 0x02}; + trie.put(key1, "a".getBytes()); + trie.put(key2, "b".getBytes()); + trie.put(key3, "c".getBytes()); + byte[] hashBefore = trie.getRootHash(); + trie.delete(key3); + byte[] hashAfterDelete = trie.getRootHash(); + Assert.assertFalse("root hash must change after delete", + Arrays.equals(hashBefore, hashAfterDelete)); + trie.put(key3, "c".getBytes()); + byte[] hashAfterReinsert = trie.getRootHash(); + Assert.assertArrayEquals("root hash must match original after re-insert", + hashBefore, hashAfterReinsert); + } + /* * Same as testOrder but without duplicate keys — verifies insertion-order * independence for the normal (non-buggy) case. @@ -179,7 +253,7 @@ public void testOrderNoDuplicate() { trie.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); } byte[] rootHash1 = trie.getRootHash(); - Collections.shuffle(value, new java.util.Random(42)); + Collections.shuffle(value, new Random(42)); TrieImpl trie2 = new TrieImpl(); for (int i : value) { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); From 5cdcebd1ab886bc4e6b000942df887db33f8ab4a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 27 Apr 2026 16:32:34 +0800 Subject: [PATCH 23/43] refactor(framework): extract parseMaxMessageSize helper Dedupe three near-identical maxMessageSize parsing blocks in applyConfigParams() (rpc, http, jsonrpc) into a single private static helper. Helper returns long to match the wider-typed http/jsonrpc fields; the rpc assignment retains an explicit (int) cast at the call site to make the gRPC int32 hard constraint visible. Net effect: ~16 lines removed, error messages guaranteed consistent across the three protocols, and adding a future protocol limit becomes a one-liner instead of a copy-paste block. Also add a brief comment on CommonParameter.maxMessageSize clarifying that the legacy field name refers to the gRPC limit, alongside the explicit httpMaxMessageSize / jsonRpcMaxMessageSize siblings. --- .../common/parameter/CommonParameter.java | 2 + .../java/org/tron/core/config/args/Args.java | 44 +++++++------------ 2 files changed, 19 insertions(+), 27 deletions(-) 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 53c5d4cfaca..6dc5f0ae52b 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -211,6 +211,8 @@ public class CommonParameter { @Getter @Setter public long maxConnectionAgeInMillis; + // Refers to RPC (gRPC) max message size; see httpMaxMessageSize / jsonRpcMaxMessageSize + // below for the HTTP / JSON-RPC counterparts. @Getter @Setter public int maxMessageSize; 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 3e181e118db..03b6a81786c 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 @@ -523,34 +523,13 @@ public static void applyConfigParams( ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) : Long.MAX_VALUE; - long rpcMaxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) - ? config.getMemorySize(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE).toBytes() - : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - if (rpcMaxMessageSize < 0 || rpcMaxMessageSize > Integer.MAX_VALUE) { - throw new TronError("node.rpc.maxMessageSize must be non-negative and <= " - + Integer.MAX_VALUE + ", got: " + rpcMaxMessageSize, PARAMETER_INIT); - } - PARAMETER.maxMessageSize = (int) rpcMaxMessageSize; - long defaultMaxMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - PARAMETER.httpMaxMessageSize = config.hasPath(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE) - ? config.getMemorySize(ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE).toBytes() - : defaultMaxMessageSize; - if (PARAMETER.httpMaxMessageSize < 0 - || PARAMETER.httpMaxMessageSize > Integer.MAX_VALUE) { - throw new TronError("node.http.maxMessageSize must be non-negative and <= " - + Integer.MAX_VALUE + ", got: " - + PARAMETER.httpMaxMessageSize, PARAMETER_INIT); - } - PARAMETER.jsonRpcMaxMessageSize = config.hasPath(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE) - ? config.getMemorySize(ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE).toBytes() - : defaultMaxMessageSize; - if (PARAMETER.jsonRpcMaxMessageSize < 0 - || PARAMETER.jsonRpcMaxMessageSize > Integer.MAX_VALUE) { - throw new TronError("node.jsonrpc.maxMessageSize must be non-negative and <= " - + Integer.MAX_VALUE + ", got: " - + PARAMETER.jsonRpcMaxMessageSize, PARAMETER_INIT); - } + PARAMETER.maxMessageSize = (int) parseMaxMessageSize( + config, ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE, defaultMaxMessageSize); + PARAMETER.httpMaxMessageSize = parseMaxMessageSize( + config, ConfigKey.NODE_HTTP_MAX_MESSAGE_SIZE, defaultMaxMessageSize); + PARAMETER.jsonRpcMaxMessageSize = parseMaxMessageSize( + config, ConfigKey.NODE_JSONRPC_MAX_MESSAGE_SIZE, defaultMaxMessageSize); PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) @@ -1258,6 +1237,17 @@ public static void clearParam() { localWitnesses = null; } + private static long parseMaxMessageSize(Config config, String key, long defaultBytes) { + long value = config.hasPath(key) + ? config.getMemorySize(key).toBytes() + : defaultBytes; + if (value < 0 || value > Integer.MAX_VALUE) { + throw new TronError(key + " must be non-negative and <= " + + Integer.MAX_VALUE + ", got: " + value, PARAMETER_INIT); + } + return value; + } + private static long getProposalExpirationTime(final Config config) { if (config.hasPath(ConfigKey.COMMITTEE_PROPOSAL_EXPIRE_TIME)) { throw new TronError("It is not allowed to configure committee.proposalExpireTime in " From d3050f99da9b4f1a346db2d11e8015185c6bb1fd Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 27 Apr 2026 16:35:21 +0800 Subject: [PATCH 24/43] ci(coverage): gate changed-line coverage (#6706) - Use diff-cover changed-line coverage as the changed-code gate; keep the overall coverage delta gate at -0.1% and JaCoCo reports as summary-only snapshots. - Handle non-Java PRs: when diff-cover reports no changed Java lines (workflow-only, docs-only, proto-only), emit NA and treat it as SKIPPED in the enforcement step so the gate does not wrongly fail. - Harden the diff-cover step: guard against empty SRC_ROOTS before invoking diff-cover, verify diff-cover.json was written, and surface exit codes in failure messages. - Clarify metric semantics in the step summary: Changed-line is LINE coverage (diff-cover) while Overall / Delta are INSTRUCTION coverage (jacoco-report). --- .github/workflows/pr-build.yml | 118 ++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef800e15ff..249bcaf28f1 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -279,6 +279,68 @@ jobs: echo "base_xmls=$BASE_XMLS" >> "$GITHUB_OUTPUT" echo "pr_xmls=$PR_XMLS" >> "$GITHUB_OUTPUT" + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Changed-line coverage (diff-cover) + id: diff-cover + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail + pip install --quiet 'diff-cover==9.2.0' + + # Ensure the base branch ref is available locally for diff-cover. + git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" + + PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort) + SRC_ROOTS=$(find . -type d -path '*/src/main/java' \ + -not -path './coverage/*' -not -path './.git/*' | sort) + if [ -z "$SRC_ROOTS" ]; then + echo "No src/main/java directories found; cannot run diff-cover." >&2 + exit 1 + fi + + set +e + diff-cover $PR_XMLS \ + --compare-branch="origin/${BASE_REF}" \ + --src-roots $SRC_ROOTS \ + --fail-under=0 \ + --json-report=diff-cover.json \ + --markdown-report=diff-cover.md + DIFF_RC=$? + set -e + + if [ ! -f diff-cover.json ]; then + echo "diff-cover did not produce JSON report (exit=${DIFF_RC})." >&2 + exit 1 + fi + + TOTAL_NUM_LINES=$(jq -r '.total_num_lines // 0' diff-cover.json) + if [ "${TOTAL_NUM_LINES}" = "0" ]; then + echo "No changed Java source lines; skipping changed-line gate." + echo "changed_line_coverage=NA" >> "$GITHUB_OUTPUT" + else + CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) + if [ -z "$CHANGED_LINE_COVERAGE" ]; then + echo "Unable to parse changed-line coverage from diff-cover.json." + exit 1 + fi + echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" + fi + + { + echo "### Changed-line Coverage (diff-cover)" + echo "" + if [ -f diff-cover.md ] && [ -s diff-cover.md ]; then + cat diff-cover.md + else + echo "_diff-cover produced no report._" + fi + } >> "$GITHUB_STEP_SUMMARY" + - name: Aggregate base coverage id: jacoco-base uses: madrapps/jacoco-report@v1.7.2 @@ -288,6 +350,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## Base Coverage Snapshot' update-comment: false @@ -300,6 +363,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## PR Code Coverage Report' update-comment: false @@ -307,7 +371,7 @@ jobs: env: BASE_OVERALL_RAW: ${{ steps.jacoco-base.outputs.coverage-overall }} PR_OVERALL_RAW: ${{ steps.jacoco-pr.outputs.coverage-overall }} - PR_CHANGED_RAW: ${{ steps.jacoco-pr.outputs.coverage-changed-files }} + CHANGED_LINE_RAW: ${{ steps.diff-cover.outputs.changed_line_coverage }} run: | set -euo pipefail @@ -329,7 +393,7 @@ jobs: # 1) Parse metrics from jacoco-report outputs BASE_OVERALL="$(sanitize "$BASE_OVERALL_RAW")" PR_OVERALL="$(sanitize "$PR_OVERALL_RAW")" - PR_CHANGED="$(sanitize "$PR_CHANGED_RAW")" + CHANGED_LINE="$(sanitize "$CHANGED_LINE_RAW")" if ! is_number "$BASE_OVERALL" || ! is_number "$PR_OVERALL"; then echo "Failed to parse coverage values: base='${BASE_OVERALL}', pr='${PR_OVERALL}'." @@ -340,18 +404,18 @@ jobs: DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }') DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}") - CHANGED_STATUS="SKIPPED (no changed coverage value)" - CHANGED_OK=1 - if [ -n "$PR_CHANGED" ] && [ "$PR_CHANGED" != "NaN" ]; then - if ! is_number "$PR_CHANGED"; then - echo "Failed to parse changed-files coverage: changed='${PR_CHANGED}'." - exit 1 - fi - CHANGED_OK=$(compare_float "${PR_CHANGED} > ${MIN_CHANGED}") - if [ "$CHANGED_OK" -eq 1 ]; then - CHANGED_STATUS="PASS (> ${MIN_CHANGED}%)" + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_OK=1 + CHANGED_LINE_STATUS="SKIPPED (no changed Java source lines)" + elif [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then + echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'." + exit 1 + else + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" else - CHANGED_STATUS="FAIL (<= ${MIN_CHANGED}%)" + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" fi fi @@ -361,13 +425,20 @@ jobs: OVERALL_STATUS="FAIL (< ${MAX_DROP}%)" fi + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_DISPLAY="NA" + else + CHANGED_LINE_DISPLAY="${CHANGED_LINE}%" + fi + METRICS_TEXT=$(cat <> "$GITHUB_STEP_SUMMARY" # 4) Decide CI pass/fail @@ -391,14 +464,9 @@ jobs: exit 1 fi - if [ -z "$PR_CHANGED" ] || [ "$PR_CHANGED" = "NaN" ]; then - echo "No changed-files coverage value detected, skip changed-files gate." - exit 0 - fi - - if [ "$CHANGED_OK" -ne 1 ]; then - echo "Coverage gate failed: changed files coverage must be > 60%." - echo "changed=${PR_CHANGED}%" + if [ "$CHANGED_LINE_OK" -ne 1 ]; then + echo "Coverage gate failed: changed-line coverage must be > 60%." + echo "changed-line=${CHANGED_LINE}%" exit 1 fi From c977f826ba592f42116df0776fcb5cda975e6171 Mon Sep 17 00:00:00 2001 From: vividctrlalt Date: Tue, 28 Apr 2026 10:48:37 +0800 Subject: [PATCH 25/43] refactor(config): replace manual parsing with ConfigBeanFactory binding (#6615) --- .../parameter/RateLimiterInitialization.java | 12 + .../org/tron/core/config/Configuration.java | 3 +- .../tron/core/config/args/BlockConfig.java | 55 + .../core/config/args/CommitteeConfig.java | 165 ++ .../tron/core/config/args/EventConfig.java | 134 ++ .../tron/core/config/args/GenesisConfig.java | 50 + .../core/config/args/LocalWitnessConfig.java | 35 + .../tron/core/config/args/MetricsConfig.java | 47 + .../org/tron/core/config/args/MiscConfig.java | 70 + .../org/tron/core/config/args/NodeConfig.java | 497 +++++ .../core/config/args/RateLimiterConfig.java | 75 + .../org/tron/core/config/args/Storage.java | 353 +--- .../tron/core/config/args/StorageConfig.java | 311 +++ .../org/tron/core/config/args/VmConfig.java | 64 + common/src/main/resources/reference.conf | 835 ++++++++ .../core/config/args/BlockConfigTest.java | 56 + .../core/config/args/CommitteeConfigTest.java | 233 +++ .../core/config/args/EventConfigTest.java | 82 + .../core/config/args/GenesisConfigTest.java | 59 + .../config/args/LocalWitnessConfigTest.java | 48 + .../core/config/args/MetricsConfigTest.java | 48 + .../tron/core/config/args/MiscConfigTest.java | 56 + .../tron/core/config/args/NodeConfigTest.java | 319 +++ .../config/args/RateLimiterConfigTest.java | 54 + .../core/config/args/StorageConfigTest.java | 142 ++ .../tron/core/config/args/VmConfigTest.java | 91 + .../java/org/tron/core/config/args/Args.java | 1786 ++++++----------- .../org/tron/core/config/args/ConfigKey.java | 331 --- .../tron/core/config/args/DynamicArgs.java | 16 +- .../org/tron/core/config/args/ArgsTest.java | 77 +- .../tron/core/exception/TronErrorTest.java | 5 +- .../src/test/resources/config-localtest.conf | 2 +- .../test/resources/config-test-dbbackup.conf | 2 +- .../src/test/resources/config-test-index.conf | 2 +- 34 files changed, 4359 insertions(+), 1756 deletions(-) create mode 100644 common/src/main/java/org/tron/core/config/args/BlockConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/CommitteeConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/EventConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/GenesisConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/MetricsConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/MiscConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/NodeConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/StorageConfig.java create mode 100644 common/src/main/java/org/tron/core/config/args/VmConfig.java create mode 100644 common/src/main/resources/reference.conf create mode 100644 common/src/test/java/org/tron/core/config/args/BlockConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/EventConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/MiscConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/NodeConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/StorageConfigTest.java create mode 100644 common/src/test/java/org/tron/core/config/args/VmConfigTest.java delete mode 100644 framework/src/main/java/org/tron/core/config/args/ConfigKey.java diff --git a/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java b/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java index 9ae7eb7db68..41388adeb7b 100644 --- a/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java +++ b/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java @@ -74,6 +74,12 @@ public HttpRateLimiterItem(ConfigObject asset) { strategy = asset.get("strategy").unwrapped().toString(); params = asset.get("paramString").unwrapped().toString(); } + + public HttpRateLimiterItem(String component, String strategy, String params) { + this.component = component; + this.strategy = strategy; + this.params = params; + } } @@ -93,5 +99,11 @@ public RpcRateLimiterItem(ConfigObject asset) { strategy = asset.get("strategy").unwrapped().toString(); params = asset.get("paramString").unwrapped().toString(); } + + public RpcRateLimiterItem(String component, String strategy, String params) { + this.component = component; + this.strategy = strategy; + this.params = params; + } } } \ No newline at end of file diff --git a/common/src/main/java/org/tron/core/config/Configuration.java b/common/src/main/java/org/tron/core/config/Configuration.java index d75fc8430f8..80735290b8c 100644 --- a/common/src/main/java/org/tron/core/config/Configuration.java +++ b/common/src/main/java/org/tron/core/config/Configuration.java @@ -48,7 +48,8 @@ public static com.typesafe.config.Config getByFileName( private static void resolveConfigFile(String fileName, File confFile) { if (confFile.exists()) { - config = ConfigFactory.parseFile(confFile); + config = ConfigFactory.parseFile(confFile) + .withFallback(ConfigFactory.defaultReference()); } else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName) != null) { config = ConfigFactory.load(fileName); diff --git a/common/src/main/java/org/tron/core/config/args/BlockConfig.java b/common/src/main/java/org/tron/core/config/args/BlockConfig.java new file mode 100644 index 00000000000..4746f390e0c --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/BlockConfig.java @@ -0,0 +1,55 @@ +package org.tron.core.config.args; + +import static org.tron.core.Constant.DEFAULT_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.Constant.MAX_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.Constant.MIN_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.exception.TronError.ErrCode.PARAMETER_INIT; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.exception.TronError; + +/** + * Block configuration bean. Field names match config.conf keys under the "block" section. + */ +@Slf4j +@Getter +@Setter +public class BlockConfig { + + private boolean needSyncCheck = false; + private long maintenanceTimeInterval = 21600000L; + private long proposalExpireTime = DEFAULT_PROPOSAL_EXPIRE_TIME; + private int checkFrozenTime = 1; + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create BlockConfig from the "block" section of the application config. + * Also checks that committee.proposalExpireTime is not used (must use block.proposalExpireTime). + */ + public static BlockConfig fromConfig(Config config) { + // Reject legacy committee.proposalExpireTime location + if (config.hasPath("committee.proposalExpireTime")) { + throw new TronError("It is not allowed to configure committee.proposalExpireTime in " + + "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT); + } + + Config blockSection = config.getConfig("block"); + BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class); + blockConfig.postProcess(); + return blockConfig; + } + + private void postProcess() { + if (proposalExpireTime <= MIN_PROPOSAL_EXPIRE_TIME + || proposalExpireTime >= MAX_PROPOSAL_EXPIRE_TIME) { + throw new TronError("The value[block.proposalExpireTime] is only allowed to " + + "be greater than " + MIN_PROPOSAL_EXPIRE_TIME + " and less than " + + MAX_PROPOSAL_EXPIRE_TIME + "!", PARAMETER_INIT); + } + } +} diff --git a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java new file mode 100644 index 00000000000..0f94e7a59eb --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -0,0 +1,165 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Committee (governance) configuration bean. + * Field names match config.conf keys under the "committee" section. + * All fields are governance proposal toggles, default 0 (disabled). + */ +@Slf4j +@Getter +@Setter +@SuppressWarnings("unused") // setters used by ConfigBeanFactory via reflection +public class CommitteeConfig { + + private long allowCreationOfContracts = 0; + private long allowMultiSign = 0; + private long allowAdaptiveEnergy = 0; + private long allowDelegateResource = 0; + private long allowSameTokenName = 0; + private long allowTvmTransferTrc10 = 0; + private long allowTvmConstantinople = 0; + private long allowTvmSolidity059 = 0; + private long forbidTransferToContract = 0; + private long allowShieldedTRC20Transaction = 0; + private long allowMarketTransaction = 0; + private long allowTransactionFeePool = 0; + private long allowBlackHoleOptimization = 0; + private long allowNewResourceModel = 0; + private long allowTvmIstanbul = 0; + private long allowProtoFilterNum = 0; + private long allowAccountStateRoot = 0; + private long changedDelegation = 0; + // NON-STANDARD NAMING: "allowPBFT" and "pBFTExpireNum" in config.conf contain + // consecutive uppercase letters ("PBFT"), which violates JavaBean naming convention. + // ConfigBeanFactory derives config keys from setter names using JavaBean rules: + // setPBFTExpireNum -> property "PBFTExpireNum" (capital P, per JavaBean spec) + // but config.conf uses "pBFTExpireNum" (lowercase p) -> mismatch -> binding fails. + // + // These two fields are excluded from auto-binding and handled manually in fromConfig(). + // TODO: Rename config keys to standard camelCase (allowPbft, pbftExpireNum) when + // PBFT feature is enabled and a breaking config change is acceptable. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private long allowPBFT = 0; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private long pBFTExpireNum = 20; + + // Only getters are exposed. No public setters — ConfigBeanFactory scans public + // setters via reflection and would derive key "PBFTExpireNum" / "AllowPBFT" + // (JavaBean uppercase rule), which does not match config keys "pBFTExpireNum" + // / "allowPBFT" and would throw. Values are assigned to fields directly in + // fromConfig() below. + public long getAllowPBFT() { return allowPBFT; } + public long getPBFTExpireNum() { return pBFTExpireNum; } + private long allowTvmFreeze = 0; + private long allowTvmVote = 0; + private long allowTvmLondon = 0; + private long allowTvmCompatibleEvm = 0; + private long allowHigherLimitForMaxCpuTimeOfOneTx = 0; + private long allowNewRewardAlgorithm = 0; + private long allowOptimizedReturnValueOfChainId = 0; + private long allowTvmShangHai = 0; + private long allowOldRewardOpt = 0; + private long allowEnergyAdjustment = 0; + private long allowStrictMath = 0; + private long consensusLogicOptimization = 0; + private long allowTvmCancun = 0; + private long allowTvmBlob = 0; + private long allowTvmOsaka = 0; + private long unfreezeDelayDays = 0; + private long allowReceiptsMerkleRoot = 0; + private long allowAccountAssetOptimization = 0; + private long allowAssetOptimization = 0; + private long allowNewReward = 0; + private long memoFee = 0; + private long allowDelegateOptimization = 0; + private long allowDynamicEnergy = 0; + private long dynamicEnergyThreshold = 0; + private long dynamicEnergyIncreaseFactor = 0; + private long dynamicEnergyMaxFactor = 0; + + // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create CommitteeConfig from the "committee" section of the application config. + * + * Note: allowPBFT and pBFTExpireNum have non-standard JavaBean naming (consecutive + * uppercase letters) which causes ConfigBeanFactory key mismatch. These two fields + * are excluded from automatic binding and handled manually after. + */ + private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum"; + private static final String ALLOW_PBFT_KEY = "allowPBFT"; + + public static CommitteeConfig fromConfig(Config config) { + Config section = config.getConfig("committee"); + + CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class); + // Ensure the manually-named fields get the right values from the original keys + cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0; + cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY) + ? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20; + + cc.postProcess(); + return cc; + } + + private void postProcess() { + // clamp unfreezeDelayDays to 0-365 + if (unfreezeDelayDays < 0) { + unfreezeDelayDays = 0; + } + if (unfreezeDelayDays > 365) { + unfreezeDelayDays = 365; + } + + // clamp allowDelegateOptimization to 0-1 + if (allowDelegateOptimization < 0) { allowDelegateOptimization = 0; } + if (allowDelegateOptimization > 1) { allowDelegateOptimization = 1; } + + // clamp allowDynamicEnergy to 0-1 + if (allowDynamicEnergy < 0) { allowDynamicEnergy = 0; } + if (allowDynamicEnergy > 1) { allowDynamicEnergy = 1; } + + // clamp dynamicEnergyThreshold to 0-100_000_000_000_000_000 + if (dynamicEnergyThreshold < 0) { dynamicEnergyThreshold = 0; } + if (dynamicEnergyThreshold > 100_000_000_000_000_000L) { + dynamicEnergyThreshold = 100_000_000_000_000_000L; + } + + // clamp dynamicEnergyIncreaseFactor to 0-10_000 + if (dynamicEnergyIncreaseFactor < 0) { dynamicEnergyIncreaseFactor = 0; } + if (dynamicEnergyIncreaseFactor > 10_000L) { dynamicEnergyIncreaseFactor = 10_000L; } + + // clamp dynamicEnergyMaxFactor to 0-100_000 + if (dynamicEnergyMaxFactor < 0) { dynamicEnergyMaxFactor = 0; } + if (dynamicEnergyMaxFactor > 100_000L) { dynamicEnergyMaxFactor = 100_000L; } + + // clamp allowNewReward to 0-1 (must run BEFORE the cross-field check below, + // which depends on allowNewReward != 1) + if (allowNewReward < 0) { allowNewReward = 0; } + if (allowNewReward > 1) { allowNewReward = 1; } + + // clamp memoFee to 0-1_000_000_000 + if (memoFee < 0) { memoFee = 0; } + if (memoFee > 1_000_000_000L) { memoFee = 1_000_000_000L; } + + // cross-field: allowOldRewardOpt requires at least one reward/vote flag + if (allowOldRewardOpt == 1 && allowNewRewardAlgorithm != 1 + && allowNewReward != 1 && allowTvmVote != 1) { + throw new IllegalArgumentException( + "At least one of the following proposals is required to be opened first: " + + "committee.allowNewRewardAlgorithm = 1" + + " or committee.allowNewReward = 1" + + " or committee.allowTvmVote = 1."); + } + } +} diff --git a/common/src/main/java/org/tron/core/config/args/EventConfig.java b/common/src/main/java/org/tron/core/config/args/EventConfig.java new file mode 100644 index 00000000000..ac1731de2dc --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -0,0 +1,134 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Event subscribe configuration bean. + * Field names match config.conf keys under "event.subscribe". + */ +@Slf4j +@Getter +@Setter +public class EventConfig { + + private boolean enable = false; + private int version = 0; + private long startSyncBlockNum = 0; + private String path = ""; + private String server = ""; + private String dbconfig = ""; + private boolean contractParse = true; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private NativeConfig nativeQueue = new NativeConfig(); + + public NativeConfig getNativeQueue() { return nativeQueue; } + // Topics list has optional fields (ethCompatible, redundancy, solidified) that + // not all items have. ConfigBeanFactory requires all bean fields to exist in config. + // Excluded from auto-binding, read manually in fromConfig(). + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private List topics = new ArrayList<>(); + + public List getTopics() { return topics; } + private FilterConfig filter = new FilterConfig(); + + @Getter + @Setter + public static class NativeConfig { + private boolean useNativeQueue = true; + private int bindport = 5555; + private int sendqueuelength = 1000; + } + + @Getter + @Setter + public static class TopicConfig { + private String triggerName = ""; + private boolean enable = false; + private String topic = ""; + private boolean solidified = false; + private boolean ethCompatible = false; + private boolean redundancy = false; + } + + @Getter + @Setter + public static class FilterConfig { + private String fromblock = ""; + private String toblock = ""; + private List contractAddress = new ArrayList<>(); + private List contractTopic = new ArrayList<>(); + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create EventConfig from the "event.subscribe" section of the application config. + * + *

Note: HOCON key "native" is a Java reserved word, so the bean field is named + * "nativeQueue" but config key is "native". We handle this manually after binding. + */ + public static EventConfig fromConfig(Config config) { + Config section = config.getConfig("event.subscribe"); + + // "native" is a Java reserved word, "topics" has optional fields per item — + // strip both before binding, read manually + String nativeKey = "native"; + String topicsKey = "topics"; + Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey) + .withoutPath("topicDefaults"); + EventConfig ec = ConfigBeanFactory.create(bindable, EventConfig.class); + + // manually bind "native" sub-section + Config nativeSection = section.hasPath(nativeKey) + ? section.getConfig(nativeKey) : ConfigFactory.empty(); + ec.nativeQueue = new NativeConfig(); + if (nativeSection.hasPath("useNativeQueue")) { + ec.nativeQueue.useNativeQueue = nativeSection.getBoolean("useNativeQueue"); + } + if (nativeSection.hasPath("bindport")) { + ec.nativeQueue.bindport = nativeSection.getInt("bindport"); + } + if (nativeSection.hasPath("sendqueuelength")) { + ec.nativeQueue.sendqueuelength = nativeSection.getInt("sendqueuelength"); + } + + // manually bind topics — each item may have optional fields + if (section.hasPath(topicsKey)) { + ec.topics = new ArrayList<>(); + for (com.typesafe.config.ConfigObject obj : section.getObjectList(topicsKey)) { + Config tc = obj.toConfig(); + TopicConfig topic = new TopicConfig(); + if (tc.hasPath("triggerName")) { + topic.triggerName = tc.getString("triggerName"); + } + if (tc.hasPath("enable")) { + topic.enable = tc.getBoolean("enable"); + } + if (tc.hasPath("topic")) { + topic.topic = tc.getString("topic"); + } + if (tc.hasPath("solidified")) { + topic.solidified = tc.getBoolean("solidified"); + } + if (tc.hasPath("ethCompatible")) { + topic.ethCompatible = tc.getBoolean("ethCompatible"); + } + if (tc.hasPath("redundancy")) { + topic.redundancy = tc.getBoolean("redundancy"); + } + ec.topics.add(topic); + } + } + + return ec; + } +} diff --git a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java new file mode 100644 index 00000000000..a17e06d5c0f --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java @@ -0,0 +1,50 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Genesis block configuration bean. + * Field names match config.conf keys under "genesis.block". + * Assets and witnesses are stored as raw bean lists; address decoding + * (e.g. Base58Check) is done in the bridge method, not here. + */ +@Slf4j +@Getter +@Setter +public class GenesisConfig { + + private String timestamp = ""; + private String parentHash = ""; + private List assets = new ArrayList<>(); + private List witnesses = new ArrayList<>(); + + @Getter + @Setter + public static class AssetConfig { + private String accountName = ""; + private String accountType = ""; + private String address = ""; + private String balance = ""; + } + + @Getter + @Setter + public static class WitnessConfig { + private String address = ""; + private String url = ""; + private long voteCount = 0; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + public static GenesisConfig fromConfig(Config config) { + Config section = config.getConfig("genesis.block"); + return ConfigBeanFactory.create(section, GenesisConfig.class); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java b/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java new file mode 100644 index 00000000000..8a2cd2ce9e4 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java @@ -0,0 +1,35 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Local witness configuration bean. + * Reads top-level config keys: localwitness, localWitnessAccountAddress, localwitnesskeystore. + * These are not under a sub-section — they are at the root of config.conf. + */ +@Slf4j +@Getter +public class LocalWitnessConfig { + + private List privateKeys = new ArrayList<>(); + private String accountAddress = null; + private List keystores = new ArrayList<>(); + + public static LocalWitnessConfig fromConfig(Config config) { + LocalWitnessConfig lw = new LocalWitnessConfig(); + if (config.hasPath("localwitness")) { + lw.privateKeys = config.getStringList("localwitness"); + } + if (config.hasPath("localWitnessAccountAddress")) { + lw.accountAddress = config.getString("localWitnessAccountAddress"); + } + if (config.hasPath("localwitnesskeystore")) { + lw.keystores = config.getStringList("localwitnesskeystore"); + } + return lw; + } +} diff --git a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java new file mode 100644 index 00000000000..5b504acdd1c --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java @@ -0,0 +1,47 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Metrics configuration bean. Field names match config.conf keys under "node.metrics". + * Contains nested sub-beans for prometheus and influxdb sections. + */ +@Slf4j +@Getter +@Setter +public class MetricsConfig { + + private boolean storageEnable = false; + private PrometheusConfig prometheus = new PrometheusConfig(); + private InfluxDbConfig influxdb = new InfluxDbConfig(); + + @Getter + @Setter + public static class PrometheusConfig { + private boolean enable = false; + private int port = 9527; + } + + @Getter + @Setter + public static class InfluxDbConfig { + private String ip = ""; + private int port = 8086; + private String database = "metrics"; + private int metricsReportInterval = 10; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create MetricsConfig from the "node.metrics" section of the application config. + */ + public static MetricsConfig fromConfig(Config config) { + Config section = config.getConfig("node.metrics"); + return ConfigBeanFactory.create(section, MetricsConfig.class); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/MiscConfig.java b/common/src/main/java/org/tron/core/config/args/MiscConfig.java new file mode 100644 index 00000000000..f6c3b200b80 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/MiscConfig.java @@ -0,0 +1,70 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.Constant; + +/** + * Miscellaneous small config domains that don't warrant their own bean class. + * Covers: storage (partial), trx, energy, crypto, seed, actuator. + * + *

These use manual reads because they span multiple unrelated config.conf + * top-level sections and some have non-standard key naming (e.g. "enery" typo). + */ +@Slf4j +@Getter +public class MiscConfig { + + private boolean needToUpdateAsset = true; + private boolean historyBalanceLookup = false; + private String trxReferenceBlock = "solid"; + private long trxExpirationTimeInMilliseconds = Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME; + private long blockNumForEnergyLimit = 4727890L; + private String cryptoEngine = Constant.ECKey_ENGINE; + private List seedNodeIpList = new ArrayList<>(); + private Set actuatorWhitelist = Collections.emptySet(); + + public static MiscConfig fromConfig(Config config) { + MiscConfig mc = new MiscConfig(); + + // storage + mc.needToUpdateAsset = !config.hasPath("storage.needToUpdateAsset") + || config.getBoolean("storage.needToUpdateAsset"); + mc.historyBalanceLookup = config.hasPath("storage.balance.history.lookup") + && config.getBoolean("storage.balance.history.lookup"); + + // trx + mc.trxReferenceBlock = config.hasPath("trx.reference.block") + ? config.getString("trx.reference.block") : "solid"; + String trxExpirationKey = "trx.expiration.timeInMilliseconds"; + if (config.hasPath(trxExpirationKey) + && config.getLong(trxExpirationKey) > 0) { + mc.trxExpirationTimeInMilliseconds = config.getLong(trxExpirationKey); + } + + // energy (note: config key has typo "enery" — preserved for backward compat) + mc.blockNumForEnergyLimit = config.hasPath("enery.limit.block.num") + ? config.getInt("enery.limit.block.num") : 4727890L; + + // crypto + mc.cryptoEngine = config.hasPath("crypto.engine") + ? config.getString("crypto.engine") : Constant.ECKey_ENGINE; + + // seed node + mc.seedNodeIpList = config.hasPath("seed.node.ip.list") + ? config.getStringList("seed.node.ip.list") : new ArrayList<>(); + + // actuator + mc.actuatorWhitelist = config.hasPath("actuator.whitelist") + ? new HashSet<>(config.getStringList("actuator.whitelist")) : Collections.emptySet(); + + return mc; + } +} 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 new file mode 100644 index 00000000000..c3305e976de --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -0,0 +1,497 @@ +package org.tron.core.config.args; + +import static org.tron.core.config.Parameter.ChainConstant.MAX_ACTIVE_WITNESS_NUM; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +// Node configuration bean for the "node" section of config.conf. +// ConfigBeanFactory auto-binds all fields including sub-beans, dot-notation keys, +// PBFT fields, and list fields. Only legacy key fallbacks and PascalCase shutdown +// keys are read manually. +@Slf4j +@Getter +@Setter +@SuppressWarnings("unused") // setters used by ConfigBeanFactory via reflection +public class NodeConfig { + + // ---- Flat scalar fields (auto-bound by ConfigBeanFactory) ---- + private String trustNode = ""; + private boolean walletExtensionApi = false; + private int syncFetchBatchNum = 2000; + private int validateSignThreadNum = 0; // 0 = auto (availableProcessors) + private int maxConnections = 30; + private int minConnections = 8; + private int minActiveConnections = 3; + private int maxConnectionsWithSameIp = 2; + private int maxHttpConnectNumber = 50; + private int minParticipationRate = 0; + private boolean openPrintLog = true; + private boolean openTransactionSort = false; + private int maxTps = 1000; + // Config key "isOpenFullTcpDisconnect" cannot auto-bind — read manually in fromConfig() + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private boolean isOpenFullTcpDisconnect = false; + + public boolean isOpenFullTcpDisconnect() { return isOpenFullTcpDisconnect; } + + // node.discovery.* — HOCON merges into node { discovery { ... } }, auto-bound + private DiscoveryConfig discovery = new DiscoveryConfig(); + + // node.shutdown.* uses PascalCase keys (BlockTime, BlockHeight, BlockCount) + // that don't match JavaBean naming. Excluded, read manually. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private String shutdownBlockTime = ""; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private long shutdownBlockHeight = -1; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private long shutdownBlockCount = -1; + + public boolean isDiscoveryEnable() { return discovery.isEnable(); } + public boolean isDiscoveryPersist() { return discovery.isPersist(); } + public String getDiscoveryExternalIp() { return discovery.getExternal().getIp(); } + public String getShutdownBlockTime() { return shutdownBlockTime; } + public long getShutdownBlockHeight() { return shutdownBlockHeight; } + public long getShutdownBlockCount() { return shutdownBlockCount; } + private int inactiveThreshold = 600; + private boolean metricsEnable = false; + private int blockProducedTimeOut = 50; + private int netMaxTrxPerSecond = 700; + private boolean nodeDetectEnable = false; + private boolean enableIpv6 = false; + private boolean effectiveCheckEnable = false; + private int maxFastForwardNum = 4; + private int tcpNettyWorkThreadNum = 0; + private int udpNettyWorkThreadNum = 1; + private ValidContractProtoConfig validContractProto = new ValidContractProtoConfig(); + private int shieldedTransInPendingMaxCounts = 10; + private long blockCacheTimeout = 60; + private long receiveTcpMinDataLength = 2048; + private ChannelConfig channel = new ChannelConfig(); + private int maxTransactionPendingSize = 2000; + private long pendingTransactionTimeout = 60000; + private int agreeNodeCount = 0; + private boolean openHistoryQueryWhenLiteFN = false; + private boolean unsolidifiedBlockCheck = false; + private int maxUnsolidifiedBlocks = 54; + private String zenTokenId = "000000"; + private boolean allowShieldedTransactionApi = true; + private double activeConnectFactor = 0.1; + private double connectFactor = 0.6; + // Legacy alias `maxActiveNodesWithSameIp` has no bean field: we only peek at it via + // section.hasPath() below. Keeping it field-less means reference.conf doesn't have to + // ship a default that would otherwise mask the modern `maxConnectionsWithSameIp` key. + + // ---- Sub-beans matching config's dot-notation nested structure ---- + private ListenConfig listen = new ListenConfig(); + private ConnectionConfig connection = new ConnectionConfig(); + private FetchBlockConfig fetchBlock = new FetchBlockConfig(); + private SolidityConfig solidity = new SolidityConfig(); + + // Convenience getters for backward compatibility with applyNodeConfig + public int getListenPort() { return listen.getPort(); } + public int getConnectionTimeout() { return connection.getTimeout(); } + public int getFetchBlockTimeout() { return fetchBlock.getTimeout(); } + public int getSolidityThreads() { return solidity.getThreads(); } + public int getChannelReadTimeout() { return channel.getRead().getTimeout(); } + public int getValidContractProtoThreads() { return validContractProto.getThreads(); } + + // ---- List fields (manually read) ---- + private List active = new ArrayList<>(); + private List passive = new ArrayList<>(); + private List fastForward = new ArrayList<>(); + private List disabledApi = new ArrayList<>(); + + // ---- Sub-object fields ---- + private P2pConfig p2p = new P2pConfig(); + private HttpConfig http = new HttpConfig(); + private RpcConfig rpc = new RpcConfig(); + private JsonRpcConfig jsonrpc = new JsonRpcConfig(); + private NodeBackupConfig backup = new NodeBackupConfig(); + private DynamicConfigSection dynamicConfig = new DynamicConfigSection(); + private DnsConfig dns = new DnsConfig(); + + // =========================================================================== + // Inner static classes for sub-beans + // =========================================================================== + + // ---- Sub-beans for dot-notation config keys ---- + // HOCON merges dot-notation into nested objects, ConfigBeanFactory auto-binds + + @Getter + @Setter + public static class DiscoveryConfig { + private boolean enable = false; + private boolean persist = false; + private ExternalConfig external = new ExternalConfig(); + + @Getter + @Setter + public static class ExternalConfig { + private String ip = ""; + } + } + + @Getter + @Setter + public static class ListenConfig { + private int port = 18888; + } + + @Getter + @Setter + public static class ConnectionConfig { + private int timeout = 2; + } + + @Getter + @Setter + public static class FetchBlockConfig { + private int timeout = 500; + } + + @Getter + @Setter + public static class SolidityConfig { + private int threads = 0; // 0 = auto (availableProcessors) + } + + @Getter + @Setter + public static class ChannelConfig { + private ReadConfig read = new ReadConfig(); + + @Getter + @Setter + public static class ReadConfig { + private int timeout = 0; + } + } + + @Getter + @Setter + public static class ValidContractProtoConfig { + private int threads = 0; // 0 = auto (availableProcessors) + } + + @Getter + @Setter + public static class P2pConfig { + private int version = 11111; + } + + @Getter + @Setter + public static class HttpConfig { + private boolean fullNodeEnable = true; + private int fullNodePort = 8090; + private boolean solidityEnable = true; + private int solidityPort = 8091; + // PBFT fields — handled manually (same naming issue as CommitteeConfig) + // Default must match CommonParameter.pBFTHttpEnable = true + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private boolean pBFTEnable = true; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private int pBFTPort = 8092; + + public boolean isPBFTEnable() { + return pBFTEnable; + } + + public void setPBFTEnable(boolean v) { + this.pBFTEnable = v; + } + + public int getPBFTPort() { + return pBFTPort; + } + + public void setPBFTPort(int v) { + this.pBFTPort = v; + } + } + + @Getter + @Setter + public static class RpcConfig { + private boolean enable = true; + private int port = 50051; + private boolean solidityEnable = true; + private int solidityPort = 50061; + // PBFT fields — handled manually + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private boolean pBFTEnable = true; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private int pBFTPort = 50071; + + public boolean isPBFTEnable() { + return pBFTEnable; + } + + public void setPBFTEnable(boolean v) { + this.pBFTEnable = v; + } + + public int getPBFTPort() { + return pBFTPort; + } + + public void setPBFTPort(int v) { + this.pBFTPort = v; + } + + private int thread = 0; + private int maxConcurrentCallsPerConnection = 2147483647; + private int flowControlWindow = 1048576; + private long maxConnectionIdleInMillis = Long.MAX_VALUE; + private long maxConnectionAgeInMillis = Long.MAX_VALUE; + private int maxMessageSize = 4194304; + private int maxHeaderListSize = 8192; + private int maxRstStream = 0; + private int secondsPerWindow = 0; + private int minEffectiveConnection = 1; + private boolean reflectionService = false; + private boolean trxCacheEnable = false; + } + + @Getter + @Setter + public static class JsonRpcConfig { + private boolean httpFullNodeEnable = false; + private int httpFullNodePort = 8545; + private boolean httpSolidityEnable = false; + private int httpSolidityPort = 8555; + // PBFT fields — handled manually + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private boolean httpPBFTEnable = false; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private int httpPBFTPort = 8565; + + public boolean isHttpPBFTEnable() { + return httpPBFTEnable; + } + + public void setHttpPBFTEnable(boolean v) { + this.httpPBFTEnable = v; + } + + public int getHttpPBFTPort() { + return httpPBFTPort; + } + + public void setHttpPBFTPort(int v) { + this.httpPBFTPort = v; + } + + private int maxBlockRange = 5000; + private int maxSubTopics = 1000; + private int maxBlockFilterNum = 50000; + } + + @Getter + @Setter + public static class NodeBackupConfig { + private int priority = 0; + private int port = 10001; + private int keepAliveInterval = 3000; + private List members = new ArrayList<>(); + } + + @Getter + @Setter + public static class DynamicConfigSection { + private boolean enable = false; + private long checkInterval = 600; + } + + @Getter + @Setter + public static class DnsConfig { + private List treeUrls = new ArrayList<>(); + private boolean publish = false; + private String dnsDomain = ""; + private String dnsPrivate = ""; + private List knownUrls = new ArrayList<>(); + private List staticNodes = new ArrayList<>(); + private int maxMergeSize = 0; + private double changeThreshold = 0.0; + private String serverType = ""; + private String accessKeyId = ""; + private String accessKeySecret = ""; + private String aliyunDnsEndpoint = ""; + private String awsRegion = ""; + private String awsHostZoneId = ""; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + // =========================================================================== + // Factory method + // =========================================================================== + + /** + * Create NodeConfig from the "node" section of the application config. + * + *

Dot-notation keys (listen.port, connection.timeout, fetchBlock.timeout, + * solidity.threads) become nested HOCON objects and cannot be auto-bound to flat + * Java fields. They are read manually after ConfigBeanFactory binding. + * + *

PBFT-named fields in http, rpc, and jsonrpc sub-beans have the same JavaBean + * naming issue as CommitteeConfig and are patched manually. + * + *

List fields (active, passive, fastForward, disabledApi) are read manually + * since ConfigBeanFactory expects typed bean lists, not string lists. + */ + public static NodeConfig fromConfig(Config config) { + Config section = config.getConfig("node"); + + // Auto-bind all fields and sub-beans. ConfigBeanFactory fails fast with a + // descriptive path on any `= null` value — external configs that use the + // HOCON null keyword should fix their config rather than rely on silent coercion. + NodeConfig nc = ConfigBeanFactory.create(section, NodeConfig.class); + + // isOpenFullTcpDisconnect: boolean "is" prefix breaks JavaBean pairing + nc.isOpenFullTcpDisconnect = getBool(section, "isOpenFullTcpDisconnect", false); + + // --- Legacy key fallbacks (backward compatibility) --- + // node.maxActiveNodes (old) -> maxConnections (new) + if (section.hasPath("maxActiveNodes")) { + nc.maxConnections = section.getInt("maxActiveNodes"); + if (section.hasPath("connectFactor")) { + nc.minConnections = (int) (nc.maxConnections * section.getDouble("connectFactor")); + } + if (section.hasPath("activeConnectFactor")) { + nc.minActiveConnections = (int) (nc.maxConnections + * section.getDouble("activeConnectFactor")); + } + } + if (section.hasPath("maxActiveNodesWithSameIp")) { + nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); + } + + // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. + // reference.conf does not ship the legacy key, so hasPath here reliably means the user + // set it in their config. When present, it overrides the modern key. + if (section.hasPath("fullNodeAllowShieldedTransaction")) { + nc.allowShieldedTransactionApi = section.getBoolean("fullNodeAllowShieldedTransaction"); + logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " + + "Please use [node.allowShieldedTransactionApi] instead."); + } + // node.shutdown.* — PascalCase keys (BlockTime, BlockHeight), cannot auto-bind + nc.shutdownBlockTime = config.hasPath("node.shutdown.BlockTime") + ? config.getString("node.shutdown.BlockTime") : ""; + nc.shutdownBlockHeight = config.hasPath("node.shutdown.BlockHeight") + ? config.getLong("node.shutdown.BlockHeight") : -1; + nc.shutdownBlockCount = config.hasPath("node.shutdown.BlockCount") + ? config.getLong("node.shutdown.BlockCount") : -1; + + + nc.postProcess(); + return nc; + } + + /** + * Post-processing: clamping, dynamic defaults, and cross-field validation. + * Runs after ConfigBeanFactory binding and manual field reads. + */ + private void postProcess() { + // rpcThreadNum: 0 = auto-detect + if (rpc.thread == 0) { + rpc.thread = (Runtime.getRuntime().availableProcessors() + 1) / 2; + } + + // validateSignThreadNum: 0 = auto-detect + if (validateSignThreadNum == 0) { + validateSignThreadNum = Runtime.getRuntime().availableProcessors(); + } + + // solidityThreads: 0 = auto-detect + if (solidity.threads == 0) { + solidity.threads = Runtime.getRuntime().availableProcessors(); + } + + // validContractProto.threads: 0 = auto-detect (matches develop Args.java:743-746) + if (validContractProto.threads == 0) { + validContractProto.threads = Runtime.getRuntime().availableProcessors(); + } + + // syncFetchBatchNum: clamp to [100, 2000] + if (syncFetchBatchNum > 2000) { + syncFetchBatchNum = 2000; + } + if (syncFetchBatchNum < 100) { + syncFetchBatchNum = 100; + } + + // blockProducedTimeOut: clamp to [30, 100] + if (blockProducedTimeOut < 30) { + blockProducedTimeOut = 30; + } + if (blockProducedTimeOut > 100) { + blockProducedTimeOut = 100; + } + + // inactiveThreshold: minimum 1 + if (inactiveThreshold < 1) { + inactiveThreshold = 1; + } + + // maxFastForwardNum: clamp to [1, MAX_ACTIVE_WITNESS_NUM] + if (maxFastForwardNum > MAX_ACTIVE_WITNESS_NUM) { + maxFastForwardNum = MAX_ACTIVE_WITNESS_NUM; + } + if (maxFastForwardNum < 1) { + maxFastForwardNum = 1; + } + + // agreeNodeCount: 0 = auto (2/3 + 1 of witnesses), clamp to max + if (agreeNodeCount == 0) { + agreeNodeCount = MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; + } + if (agreeNodeCount > MAX_ACTIVE_WITNESS_NUM) { + agreeNodeCount = MAX_ACTIVE_WITNESS_NUM; + } + + // dynamicConfigCheckInterval: minimum 600 + if (dynamicConfig.checkInterval <= 0) { + dynamicConfig.checkInterval = 600; + } + } + + // =========================================================================== + // Helper methods for safe config reads + // =========================================================================== + + private static int getInt(Config config, String path, int defaultValue) { + return config.hasPath(path) ? config.getInt(path) : defaultValue; + } + + private static long getLong(Config config, String path, long defaultValue) { + return config.hasPath(path) ? config.getLong(path) : defaultValue; + } + + private static boolean getBool(Config config, String path, boolean defaultValue) { + return config.hasPath(path) ? config.getBoolean(path) : defaultValue; + } + + private static String getString(Config config, String path, String defaultValue) { + return config.hasPath(path) ? config.getString(path) : defaultValue; + } + +} diff --git a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java new file mode 100644 index 00000000000..eed5ef1898b --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -0,0 +1,75 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Rate limiter configuration bean. + * Field names match config.conf keys under "rate.limiter". + */ +@Slf4j +@Getter +@Setter +public class RateLimiterConfig { + + private GlobalConfig global = new GlobalConfig(); + private P2pRateLimitConfig p2p = new P2pRateLimitConfig(); + private List http = new ArrayList<>(); + private List rpc = new ArrayList<>(); + + @Getter + @Setter + public static class GlobalConfig { + private int qps = 50000; + private IpConfig ip = new IpConfig(); + private ApiConfig api = new ApiConfig(); + + @Getter + @Setter + public static class IpConfig { + private int qps = 10000; + } + + @Getter + @Setter + public static class ApiConfig { + private int qps = 1000; + } + } + + @Getter + @Setter + public static class P2pRateLimitConfig { + private double syncBlockChain = 3.0; + private double fetchInvData = 3.0; + private double disconnect = 1.0; + } + + @Getter + @Setter + public static class HttpRateLimitItem { + private String component = ""; + private String strategy = ""; + private String paramString = ""; + } + + @Getter + @Setter + public static class RpcRateLimitItem { + private String component = ""; + private String strategy = ""; + private String paramString = ""; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + public static RateLimiterConfig fromConfig(Config config) { + Config section = config.getConfig("rate.limiter"); + return ConfigBeanFactory.create(section, RateLimiterConfig.class); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 116074c62ee..782a0ef07c8 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -18,7 +18,6 @@ import com.google.common.collect.Maps; import com.google.protobuf.ByteString; import com.typesafe.config.Config; -import com.typesafe.config.ConfigObject; import java.io.File; import java.util.List; import java.util.Map; @@ -26,7 +25,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.iq80.leveldb.CompressionType; import org.iq80.leveldb.Options; import org.tron.common.cache.CacheStrategies; @@ -46,59 +44,12 @@ @Slf4j(topic = "db") public class Storage { - /** - * Keys (names) of database config - */ - private static final String DB_DIRECTORY_CONFIG_KEY = "storage.db.directory"; - private static final String DB_ENGINE_CONFIG_KEY = "storage.db.engine"; - private static final String DB_SYNC_CONFIG_KEY = "storage.db.sync"; - private static final String INDEX_DIRECTORY_CONFIG_KEY = "storage.index.directory"; - private static final String INDEX_SWITCH_CONFIG_KEY = "storage.index.switch"; - private static final String TRANSACTIONHISTORY_SWITCH_CONFIG_KEY = "storage.transHistory.switch"; - private static final String ESTIMATED_TRANSACTIONS_CONFIG_KEY = - "storage.txCache.estimatedTransactions"; - private static final String SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY = "storage.snapshot.maxFlushCount"; - private static final String PROPERTIES_CONFIG_KEY = "storage.properties"; - private static final String PROPERTIES_CONFIG_DB_KEY = "storage"; - private static final String PROPERTIES_CONFIG_DEFAULT_KEY = "default"; - private static final String PROPERTIES_CONFIG_DEFAULT_M_KEY = "defaultM"; - private static final String PROPERTIES_CONFIG_DEFAULT_L_KEY = "defaultL"; - private static final String DEFAULT_TRANSACTIONHISTORY_SWITCH = "on"; - - private static final String NAME_CONFIG_KEY = "name"; - private static final String PATH_CONFIG_KEY = "path"; - private static final String CREATE_IF_MISSING_CONFIG_KEY = "createIfMissing"; - private static final String PARANOID_CHECKS_CONFIG_KEY = "paranoidChecks"; - private static final String VERITY_CHECK_SUMS_CONFIG_KEY = "verifyChecksums"; - private static final String COMPRESSION_TYPE_CONFIG_KEY = "compressionType"; - private static final String BLOCK_SIZE_CONFIG_KEY = "blockSize"; - private static final String WRITE_BUFFER_SIZE_CONFIG_KEY = "writeBufferSize"; - private static final String CACHE_SIZE_CONFIG_KEY = "cacheSize"; - private static final String MAX_OPEN_FILES_CONFIG_KEY = "maxOpenFiles"; - private static final String EVENT_SUBSCRIBE_CONTRACT_PARSE = "event.subscribe.contractParse"; - - private static final String CHECKPOINT_VERSION_KEY = "storage.checkpoint.version"; - private static final String CHECKPOINT_SYNC_KEY = "storage.checkpoint.sync"; - - private static final String CACHE_STRATEGIES = "storage.cache.strategies"; - public static final String TX_CACHE_INIT_OPTIMIZATION = "storage.txCache.initOptimization"; - - private static final String MERKLE_ROOT = "storage.merkleRoot"; - - /** - * Default values of directory - */ - private static final String DEFAULT_DB_ENGINE = "LEVELDB"; - private static final boolean DEFAULT_DB_SYNC = false; - private static final boolean DEFAULT_EVENT_SUBSCRIBE_CONTRACT_PARSE = true; - private static final String DEFAULT_DB_DIRECTORY = "database"; - private static final String DEFAULT_INDEX_DIRECTORY = "index"; private static final String DEFAULT_INDEX_SWITCH = "on"; - private static final int DEFAULT_CHECKPOINT_VERSION = 1; - private static final boolean DEFAULT_CHECKPOINT_SYNC = true; - private static final int DEFAULT_ESTIMATED_TRANSACTIONS = 1000; - private static final int DEFAULT_SNAPSHOT_MAX_FLUSH_COUNT = 1; - private Config storage; + + // Optional per-tier LevelDB option overrides, read from StorageConfig bean + private StorageConfig.DbOptionOverride defaultDbOption; + private StorageConfig.DbOptionOverride defaultMDbOption; + private StorageConfig.DbOptionOverride defaultLDbOption; /** * Database storage directory: /path/to/{dbDirectory} @@ -172,92 +123,13 @@ public class Storage { // db root private final Map dbRoots = Maps.newConcurrentMap(); - public static String getDbEngineFromConfig(final Config config) { - return config.hasPath(DB_ENGINE_CONFIG_KEY) - ? config.getString(DB_ENGINE_CONFIG_KEY) : DEFAULT_DB_ENGINE; - } - - public static Boolean getDbVersionSyncFromConfig(final Config config) { - return config.hasPath(DB_SYNC_CONFIG_KEY) - ? config.getBoolean(DB_SYNC_CONFIG_KEY) : DEFAULT_DB_SYNC; - } - - public static int getSnapshotMaxFlushCountFromConfig(final Config config) { - if (!config.hasPath(SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY)) { - return DEFAULT_SNAPSHOT_MAX_FLUSH_COUNT; - } - int maxFlushCountConfig = config.getInt(SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY); - if (maxFlushCountConfig <= 0) { - throw new IllegalArgumentException("MaxFlushCount value can not be negative or zero!"); - } - if (maxFlushCountConfig > 500) { - throw new IllegalArgumentException("MaxFlushCount value must not exceed 500!"); - } - return maxFlushCountConfig; - } - - public static Boolean getContractParseSwitchFromConfig(final Config config) { - return config.hasPath(EVENT_SUBSCRIBE_CONTRACT_PARSE) - ? config.getBoolean(EVENT_SUBSCRIBE_CONTRACT_PARSE) - : DEFAULT_EVENT_SUBSCRIBE_CONTRACT_PARSE; - } - - public static String getDbDirectoryFromConfig(final Config config) { - return config.hasPath(DB_DIRECTORY_CONFIG_KEY) - ? config.getString(DB_DIRECTORY_CONFIG_KEY) : DEFAULT_DB_DIRECTORY; - } - - public static String getIndexDirectoryFromConfig(final Config config) { - return config.hasPath(INDEX_DIRECTORY_CONFIG_KEY) - ? config.getString(INDEX_DIRECTORY_CONFIG_KEY) : DEFAULT_INDEX_DIRECTORY; - } - - public static String getIndexSwitchFromConfig(final Config config) { - return config.hasPath(INDEX_SWITCH_CONFIG_KEY) - && StringUtils.isNotEmpty(config.getString(INDEX_SWITCH_CONFIG_KEY)) - ? config.getString(INDEX_SWITCH_CONFIG_KEY) : DEFAULT_INDEX_SWITCH; - } - - public static String getTransactionHistorySwitchFromConfig(final Config config) { - return config.hasPath(TRANSACTIONHISTORY_SWITCH_CONFIG_KEY) - ? config.getString(TRANSACTIONHISTORY_SWITCH_CONFIG_KEY) - : DEFAULT_TRANSACTIONHISTORY_SWITCH; - } - - public static int getCheckpointVersionFromConfig(final Config config) { - return config.hasPath(CHECKPOINT_VERSION_KEY) - ? config.getInt(CHECKPOINT_VERSION_KEY) - : DEFAULT_CHECKPOINT_VERSION; - } - - public static boolean getCheckpointSyncFromConfig(final Config config) { - return config.hasPath(CHECKPOINT_SYNC_KEY) - ? config.getBoolean(CHECKPOINT_SYNC_KEY) - : DEFAULT_CHECKPOINT_SYNC; - } - - public static int getEstimatedTransactionsFromConfig(final Config config) { - if (!config.hasPath(ESTIMATED_TRANSACTIONS_CONFIG_KEY)) { - return DEFAULT_ESTIMATED_TRANSACTIONS; - } - int estimatedTransactions = config.getInt(ESTIMATED_TRANSACTIONS_CONFIG_KEY); - if (estimatedTransactions > 10000) { - estimatedTransactions = 10000; - } else if (estimatedTransactions < 100) { - estimatedTransactions = 100; - } - return estimatedTransactions; - } - - public static boolean getTxCacheInitOptimizationFromConfig(final Config config) { - return config.hasPath(TX_CACHE_INIT_OPTIMIZATION) - && config.getBoolean(TX_CACHE_INIT_OPTIMIZATION); - } - - - public void setCacheStrategies(Config config) { - if (config.hasPath(CACHE_STRATEGIES)) { - config.getConfig(CACHE_STRATEGIES).resolve().entrySet().forEach(c -> + /** + * Accepts raw storage Config sub-tree because cache.strategies has dynamic keys + * (CacheType enum names) that ConfigBeanFactory cannot bind to fixed bean fields. + */ + public void setCacheStrategies(Config storageSection) { + if (storageSection.hasPath("cache.strategies")) { + storageSection.getConfig("cache.strategies").resolve().entrySet().forEach(c -> this.cacheStrategies.put(CacheType.valueOf(c.getKey()), c.getValue().unwrapped().toString())); } @@ -271,138 +143,75 @@ public Sha256Hash getDbRoot(String dbName, Sha256Hash defaultV) { return this.dbRoots.getOrDefault(dbName, defaultV); } - public void setDbRoots(Config config) { - if (config.hasPath(MERKLE_ROOT)) { - config.getConfig(MERKLE_ROOT).resolve().entrySet().forEach(c -> - this.dbRoots.put(c.getKey(), Sha256Hash.wrap( + /** + * Accepts raw storage Config sub-tree because merkleRoot has dynamic keys + * (database names) that ConfigBeanFactory cannot bind to fixed bean fields. + */ + public void setDbRoots(Config storageSection) { + if (storageSection.hasPath("merkleRoot")) { + storageSection.getConfig("merkleRoot").resolve().entrySet().forEach(c -> + this.dbRoots.put(c.getKey(), Sha256Hash.wrap( ByteString.fromHex(c.getValue().unwrapped().toString())))); } } - private Property createProperty(final ConfigObject conf) { - + /** + * Create Property from StorageConfig.PropertyConfig bean. + */ + private Property createPropertyFromBean(StorageConfig.PropertyConfig pc) { Property property = new Property(); - // Database name must be set - if (!conf.containsKey(NAME_CONFIG_KEY)) { + if (pc.getName().isEmpty()) { throw new IllegalArgumentException("[storage.properties] database name must be set."); } - property.setName(conf.get(NAME_CONFIG_KEY).unwrapped().toString()); - - // Check writable permission of path - if (conf.containsKey(PATH_CONFIG_KEY)) { - String path = conf.get(PATH_CONFIG_KEY).unwrapped().toString(); + property.setName(pc.getName()); + if (!pc.getPath().isEmpty()) { + String path = pc.getPath(); File file = new File(path); if (!file.exists() && !file.mkdirs()) { throw new IllegalArgumentException( String.format("[storage.properties] can not create storage path: %s", path)); } - if (!file.canWrite()) { throw new IllegalArgumentException( String.format("[storage.properties] permission denied to write to: %s ", path)); } - property.setPath(path); } - // Check, get and set fields of Options Options dbOptions = newDefaultDbOptions(property.getName()); - - setIfNeeded(conf, dbOptions); - + applyPropertyOptions(pc, dbOptions); property.setDbOptions(dbOptions); return property; } - private static void setIfNeeded(ConfigObject conf, Options dbOptions) { - if (conf.containsKey(CREATE_IF_MISSING_CONFIG_KEY)) { - dbOptions.createIfMissing( - Boolean.parseBoolean( - conf.get(CREATE_IF_MISSING_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(PARANOID_CHECKS_CONFIG_KEY)) { - dbOptions.paranoidChecks( - Boolean.parseBoolean( - conf.get(PARANOID_CHECKS_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(VERITY_CHECK_SUMS_CONFIG_KEY)) { - dbOptions.verifyChecksums( - Boolean.parseBoolean( - conf.get(VERITY_CHECK_SUMS_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(COMPRESSION_TYPE_CONFIG_KEY)) { - String param = conf.get(COMPRESSION_TYPE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.compressionType( - CompressionType.getCompressionTypeByPersistentId(Integer.parseInt(param))); - } catch (NumberFormatException e) { - throwIllegalArgumentException(COMPRESSION_TYPE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(BLOCK_SIZE_CONFIG_KEY)) { - String param = conf.get(BLOCK_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.blockSize(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(BLOCK_SIZE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(WRITE_BUFFER_SIZE_CONFIG_KEY)) { - String param = conf.get(WRITE_BUFFER_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.writeBufferSize(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(WRITE_BUFFER_SIZE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(CACHE_SIZE_CONFIG_KEY)) { - String param = conf.get(CACHE_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.cacheSize(Long.parseLong(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(CACHE_SIZE_CONFIG_KEY, Long.class, param); - } - } - - if (conf.containsKey(MAX_OPEN_FILES_CONFIG_KEY)) { - String param = conf.get(MAX_OPEN_FILES_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.maxOpenFiles(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(MAX_OPEN_FILES_CONFIG_KEY, Integer.class, param); - } - } + /** + * Apply LevelDB options from PropertyConfig bean values. + */ + private static void applyPropertyOptions(StorageConfig.PropertyConfig pc, Options dbOptions) { + dbOptions.createIfMissing(pc.isCreateIfMissing()); + dbOptions.paranoidChecks(pc.isParanoidChecks()); + dbOptions.verifyChecksums(pc.isVerifyChecksums()); + dbOptions.compressionType( + CompressionType.getCompressionTypeByPersistentId(pc.getCompressionType())); + dbOptions.blockSize(pc.getBlockSize()); + dbOptions.writeBufferSize(pc.getWriteBufferSize()); + dbOptions.cacheSize(pc.getCacheSize()); + dbOptions.maxOpenFiles(pc.getMaxOpenFiles()); } - private static void throwIllegalArgumentException(String param, Class type, String actual) { - throw new IllegalArgumentException( - String.format("[storage.properties] %s must be %s type, actual: %s.", - param, type.getSimpleName(), actual)); - } /** - * Set propertyMap of Storage object from Config - * - * @param config Config object from "config.conf" file + * Set propertyMap of Storage object from Config via StorageConfig bean. */ - public void setPropertyMapFromConfig(final Config config) { - if (config.hasPath(PROPERTIES_CONFIG_KEY)) { - propertyMap = config.getObjectList(PROPERTIES_CONFIG_KEY).stream() - .map(this::createProperty) + /** + * Set propertyMap from StorageConfig bean list. No Config parameter needed. + */ + public void setPropertyMapFromBean(List props) { + if (props != null && !props.isEmpty()) { + propertyMap = props.stream() + .map(this::createPropertyFromBean) .collect(Collectors.toMap(Property::getName, p -> p)); } } @@ -423,30 +232,60 @@ public void deleteAllStoragePaths() { } } - public void setDefaultDbOptions(final Config config) { + /** + * Initialize default LevelDB options and store optional per-tier overrides + * from StorageConfig bean (no raw Config needed). + */ + public void setDefaultDbOptions(StorageConfig sc) { this.defaultDbOptions = DbOptionalsUtils.createDefaultDbOptions(); - storage = config.getConfig(PROPERTIES_CONFIG_DB_KEY); + this.defaultDbOption = sc.getDefaultDbOption(); + this.defaultMDbOption = sc.getDefaultMDbOption(); + this.defaultLDbOption = sc.getDefaultLDbOption(); } - public Options newDefaultDbOptions(String name ) { - // first fetch origin default - Options options = DbOptionalsUtils.newDefaultDbOptions(name, this.defaultDbOptions); + public Options newDefaultDbOptions(String name) { + Options options = DbOptionalsUtils.newDefaultDbOptions(name, this.defaultDbOptions); - // then fetch from config for default - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_KEY)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_KEY), options); + if (defaultDbOption != null) { + applyDbOptionOverride(defaultDbOption, options); } - - // check if has middle config - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_M_KEY) && DbOptionalsUtils.DB_M.contains(name)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_M_KEY), options); - + if (defaultMDbOption != null && DbOptionalsUtils.DB_M.contains(name)) { + applyDbOptionOverride(defaultMDbOption, options); } - // check if has large config - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_L_KEY) && DbOptionalsUtils.DB_L.contains(name)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_L_KEY), options); + if (defaultLDbOption != null && DbOptionalsUtils.DB_L.contains(name)) { + applyDbOptionOverride(defaultLDbOption, options); } return options; } + + // Apply only user-specified overrides (non-null fields) to LevelDB Options. + private static void applyDbOptionOverride( + StorageConfig.DbOptionOverride o, Options dbOptions) { + if (o.getCreateIfMissing() != null) { + dbOptions.createIfMissing(o.getCreateIfMissing()); + } + if (o.getParanoidChecks() != null) { + dbOptions.paranoidChecks(o.getParanoidChecks()); + } + if (o.getVerifyChecksums() != null) { + dbOptions.verifyChecksums(o.getVerifyChecksums()); + } + if (o.getCompressionType() != null) { + dbOptions.compressionType( + CompressionType.getCompressionTypeByPersistentId(o.getCompressionType())); + } + if (o.getBlockSize() != null) { + dbOptions.blockSize(o.getBlockSize()); + } + if (o.getWriteBufferSize() != null) { + dbOptions.writeBufferSize(o.getWriteBufferSize()); + } + if (o.getCacheSize() != null) { + dbOptions.cacheSize(o.getCacheSize()); + } + if (o.getMaxOpenFiles() != null) { + dbOptions.maxOpenFiles(o.getMaxOpenFiles()); + } + } } diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java new file mode 100644 index 00000000000..2517f4d10d7 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -0,0 +1,311 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigObject; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.tron.common.math.StrictMathWrapper; + +/** + * Storage configuration bean. + * Field names match config.conf keys under the "storage" section. + * Covers db, index, properties, dbSettings, backup, checkpoint, txCache, etc. + */ +@Slf4j +@Getter +@Setter +public class StorageConfig { + + private DbConfig db = new DbConfig(); + private IndexConfig index = new IndexConfig(); + private TransHistoryConfig transHistory = new TransHistoryConfig(); + private boolean needToUpdateAsset = true; + private DbSettingsConfig dbSettings = new DbSettingsConfig(); + private BackupConfig backup = new BackupConfig(); + private BalanceConfig balance = new BalanceConfig(); + private CheckpointConfig checkpoint = new CheckpointConfig(); + private SnapshotConfig snapshot = new SnapshotConfig(); + private TxCacheConfig txCache = new TxCacheConfig(); + private List properties = new ArrayList<>(); + + // merkleRoot is a nested object (e.g. { reward-vi = "hash..." }) not a string. + // Excluded from auto-binding, handled by Storage class directly. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private Object merkleRoot; + + // Raw storage config sub-tree, kept for setCacheStrategies/setDbRoots which + // have dynamic keys that ConfigBeanFactory cannot bind. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private Config rawStorageConfig; + + public Config getRawStorageConfig() { + return rawStorageConfig; + } + + // LevelDB per-database option overrides (default, defaultM, defaultL). + // Excluded from auto-binding: optional partial overrides that ConfigBeanFactory cannot handle. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private DbOptionOverride defaultDbOption; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private DbOptionOverride defaultMDbOption; + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private DbOptionOverride defaultLDbOption; + + public DbOptionOverride getDefaultDbOption() { return defaultDbOption; } + public DbOptionOverride getDefaultMDbOption() { return defaultMDbOption; } + public DbOptionOverride getDefaultLDbOption() { return defaultLDbOption; } + + @Getter + @Setter + public static class DbConfig { + private String engine = "LEVELDB"; + private boolean sync = false; + private String directory = "database"; + } + + @Getter + @Setter + public static class IndexConfig { + private String directory = "index"; + // "switch" is a Java keyword, but HOCON key is "index.switch" + // ConfigBeanFactory would look for setSwitch which works fine in Java + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private String switchValue = "on"; + + public String getSwitch() { + return switchValue; + } + + public void setSwitch(String v) { + this.switchValue = v; + } + } + + @Getter + @Setter + public static class TransHistoryConfig { + // "switch" is a Java keyword — same handling as IndexConfig + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private String switchValue = "on"; + + public String getSwitch() { + return switchValue; + } + + public void setSwitch(String v) { + this.switchValue = v; + } + } + + @Getter + @Setter + public static class DbSettingsConfig { + private int levelNumber = 7; + private int compactThreads = 0; // 0 = auto: max(availableProcessors, 1) + private int blocksize = 16; + private long maxBytesForLevelBase = 256; + private double maxBytesForLevelMultiplier = 10; + private int level0FileNumCompactionTrigger = 2; + private long targetFileSizeBase = 64; + private int targetFileSizeMultiplier = 1; + private int maxOpenFiles = 5000; + + // Expand 0 → auto-detected processor count. Mirrors develop Args.java:1609-1611. + void postProcess() { + if (compactThreads == 0) { + compactThreads = StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1); + } + } + } + + @Getter + @Setter + public static class BackupConfig { + private boolean enable = false; + private String propPath = "prop.properties"; + private String bak1path = "bak1/database/"; + private String bak2path = "bak2/database/"; + private int frequency = 10000; + } + + @Getter + @Setter + public static class BalanceConfig { + private HistoryConfig history = new HistoryConfig(); + + @Getter + @Setter + public static class HistoryConfig { + private boolean lookup = false; + } + } + + @Getter + @Setter + public static class CheckpointConfig { + private int version = 1; + private boolean sync = true; + } + + @Getter + @Setter + public static class SnapshotConfig { + private int maxFlushCount = 1; + + // Reject out-of-range values. Mirrors develop Storage.getSnapshotMaxFlushCountFromConfig. + void postProcess() { + if (maxFlushCount <= 0) { + throw new IllegalArgumentException("MaxFlushCount value can not be negative or zero!"); + } + if (maxFlushCount > 500) { + throw new IllegalArgumentException("MaxFlushCount value must not exceed 500!"); + } + } + } + + @Getter + @Setter + public static class TxCacheConfig { + private int estimatedTransactions = 1000; + private boolean initOptimization = false; + + // Clamp to [100, 10000]. Mirrors develop Storage.getEstimatedTransactionsFromConfig. + void postProcess() { + if (estimatedTransactions > 10000) { + estimatedTransactions = 10000; + } else if (estimatedTransactions < 100) { + estimatedTransactions = 100; + } + } + } + + @Getter + @Setter + public static class PropertyConfig { + private String name = ""; + private String path = ""; + private boolean createIfMissing = true; + private boolean paranoidChecks = true; + private boolean verifyChecksums = true; + private int compressionType = 1; + private int blockSize = 4096; + private int writeBufferSize = 10485760; + private long cacheSize = 10485760; + private int maxOpenFiles = 100; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + public static StorageConfig fromConfig(Config config) { + Config section = config.getConfig("storage"); + + StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); + sc.rawStorageConfig = section; + + // Read optional LevelDB option overrides (default, defaultM, defaultL). + sc.defaultDbOption = readDbOption(section, "default"); + sc.defaultMDbOption = readDbOption(section, "defaultM"); + sc.defaultLDbOption = readDbOption(section, "defaultL"); + + sc.dbSettings.postProcess(); + sc.snapshot.postProcess(); + sc.txCache.postProcess(); + return sc; + } + + // Partial LevelDB option override for default/defaultM/defaultL. + // Uses boxed types so null means "not set by user, keep existing value". + @Getter + @Setter + public static class DbOptionOverride { + private Boolean createIfMissing; + private Boolean paranoidChecks; + private Boolean verifyChecksums; + private Integer compressionType; + private Integer blockSize; + private Integer writeBufferSize; + private Long cacheSize; + private Integer maxOpenFiles; + } + + // Read optional LevelDB option override (default/defaultM/defaultL). + // Not bean-bound: users may only set a subset of keys (e.g. just maxOpenFiles), + // ConfigBeanFactory requires all fields present so partial overrides would fail. + private static DbOptionOverride readDbOption(Config section, String key) { + if (!section.hasPath(key)) { + return null; + } + ConfigObject conf = section.getObject(key); + DbOptionOverride o = new DbOptionOverride(); + if (conf.containsKey("createIfMissing")) { + o.setCreateIfMissing( + Boolean.parseBoolean(conf.get("createIfMissing").unwrapped().toString())); + } + if (conf.containsKey("paranoidChecks")) { + o.setParanoidChecks( + Boolean.parseBoolean(conf.get("paranoidChecks").unwrapped().toString())); + } + if (conf.containsKey("verifyChecksums")) { + o.setVerifyChecksums( + Boolean.parseBoolean(conf.get("verifyChecksums").unwrapped().toString())); + } + if (conf.containsKey("compressionType")) { + String param = conf.get("compressionType").unwrapped().toString(); + try { + o.setCompressionType(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("compressionType", Integer.class, param); + } + } + if (conf.containsKey("blockSize")) { + String param = conf.get("blockSize").unwrapped().toString(); + try { + o.setBlockSize(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("blockSize", Integer.class, param); + } + } + if (conf.containsKey("writeBufferSize")) { + String param = conf.get("writeBufferSize").unwrapped().toString(); + try { + o.setWriteBufferSize(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("writeBufferSize", Integer.class, param); + } + } + if (conf.containsKey("cacheSize")) { + String param = conf.get("cacheSize").unwrapped().toString(); + try { + o.setCacheSize(Long.parseLong(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("cacheSize", Long.class, param); + } + } + if (conf.containsKey("maxOpenFiles")) { + String param = conf.get("maxOpenFiles").unwrapped().toString(); + try { + o.setMaxOpenFiles(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("maxOpenFiles", Integer.class, param); + } + } + return o; + } + + private static void throwIllegalArgumentException(String param, Class type, String actual) { + throw new IllegalArgumentException( + String.format("[storage.properties] %s must be %s type, actual: %s.", + param, type.getSimpleName(), actual)); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java new file mode 100644 index 00000000000..d583cf4c601 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -0,0 +1,64 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * VM configuration bean. Field names match config.conf keys under the "vm" section. + * Bound automatically via ConfigBeanFactory — no manual key constants needed. + */ +@Slf4j +@Getter +@Setter +public class VmConfig { + + private boolean supportConstant = false; + private long maxEnergyLimitForConstant = 100_000_000L; + private int lruCacheSize = 500; + private double minTimeRatio = 0.0; + private double maxTimeRatio = 5.0; + private int longRunningTime = 10; + private boolean estimateEnergy = false; + private int estimateEnergyMaxRetry = 3; + private boolean vmTrace = false; + private boolean saveInternalTx = false; + private boolean saveFeaturedInternalTx = false; + private boolean saveCancelAllUnfreezeV2Details = false; + + /** + * Create VmConfig from the "vm" section of the application config. + * Defaults come from reference.conf (loaded globally via Configuration.java), + * so no per-bean DEFAULTS needed. + */ + public static VmConfig fromConfig(Config config) { + Config vmSection = config.getConfig("vm"); + VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); + vmConfig.postProcess(); + return vmConfig; + } + + private void postProcess() { + // clamp maxEnergyLimitForConstant + if (maxEnergyLimitForConstant < 3_000_000L) { + maxEnergyLimitForConstant = 3_000_000L; + } + + // clamp estimateEnergyMaxRetry to 0-10 + if (estimateEnergyMaxRetry < 0) { + estimateEnergyMaxRetry = 0; + } + if (estimateEnergyMaxRetry > 10) { + estimateEnergyMaxRetry = 10; + } + + // cross-field dependency warning + if (saveCancelAllUnfreezeV2Details + && (!saveInternalTx || !saveFeaturedInternalTx)) { + logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as " + + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); + } + } +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf new file mode 100644 index 00000000000..11970a0a673 --- /dev/null +++ b/common/src/main/resources/reference.conf @@ -0,0 +1,835 @@ +# ============================================================================= +# reference.conf — Full default configuration for java-tron +# ============================================================================= +# +# This file defines the default value for every configuration parameter. +# It is packaged inside the jar and loaded automatically via Typesafe Config's +# standard mechanism: ConfigFactory.defaultReference(). +# +# Loading priority (highest wins): +# 1. User's external config file (e.g. config.conf passed via -c flag) +# 2. This file (reference.conf, bundled in jar) +# +# When a user's config.conf omits a parameter, the value from this file is +# used as the fallback. This ensures the node always has a complete and valid +# configuration, even if the user only overrides a few parameters. +# +# Maintenance rules: +# - Every parameter that the code reads must have an entry here +# - Values must match the bean field initializers in the corresponding +# XxxConfig.java classes (VmConfig, NodeConfig, CommitteeConfig, etc.) +# - Keep the section order and key order identical to config.conf for +# easy side-by-side comparison +# - When adding a new parameter: add it here AND in the bean class +# +# Key naming rules (required for ConfigBeanFactory auto-binding): +# - Use standard camelCase: maxConnections, syncFetchBatchNum, etc. +# +# Keys that cannot auto-bind (handled manually in bean fromConfig): +# +# 1. committee.pBFTExpireNum — lowercase "p" then uppercase "BFT": +# setPBFTExpireNum -> property "PBFTExpireNum" (capital P), +# mismatches config key "pBFTExpireNum" (lowercase p). +# +# 2. node.isOpenFullTcpDisconnect — boolean "is" prefix: +# getter isOpenFullTcpDisconnect() -> property "openFullTcpDisconnect", +# mismatches config key "isOpenFullTcpDisconnect". +# +# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — PascalCase keys: +# setBlockTime -> property "blockTime", mismatches "BlockTime". +# +# ============================================================================= + +net { + # type is deprecated and has no effect. + # type = mainnet +} + +storage { + # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) + db.engine = "LEVELDB" + db.sync = false + db.directory = "database" + + # Index directory (legacy, not consumed by any runtime code, kept for CLI/test compatibility) + index.directory = "index" + index.switch = "on" + + # Whether to write transaction result in transactionRetStore + transHistory.switch = "on" + + # Per-database LevelDB option overrides. Default: empty (all databases use global defaults). + # setting can improve leveldb performance .... start, deprecated for arm + # node: if this will increase process fds, you may check your ulimit if 'too many open files' error occurs + # see https://github.com/tronprotocol/tips/blob/master/tip-343.md for detail + # if you find block sync has lower performance, you can try this settings + # default = { + # maxOpenFiles = 100 + # } + # defaultM = { + # maxOpenFiles = 500 + # } + # defaultL = { + # maxOpenFiles = 1000 + # } + # setting can improve leveldb performance .... end, deprecated for arm + + # Example per-database overrides: + # { + # name = "account", + # path = "storage_directory_test", + # createIfMissing = true, + # paranoidChecks = true, + # verifyChecksums = true, + # compressionType = 1, // compressed with snappy + # blockSize = 4096, // 4 KB = 4 * 1024 B + # writeBufferSize = 10485760, // 10 MB = 10 * 1024 * 1024 B + # cacheSize = 10485760, // 10 MB = 10 * 1024 * 1024 B + # maxOpenFiles = 100 + # } + properties = [] + + needToUpdateAsset = true + + # RocksDB settings (only used when db.engine = "ROCKSDB") + # Strongly recommend NOT modifying unless you know every item's meaning clearly. + dbSettings = { + levelNumber = 7 + compactThreads = 0 // 0 = auto: max(availableProcessors, 1) + blocksize = 16 // n * KB + maxBytesForLevelBase = 256 // n * MB + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 2 + targetFileSizeBase = 64 // n * MB + targetFileSizeMultiplier = 1 + maxOpenFiles = 5000 + } + + balance.history.lookup = false + + # Checkpoint version for snapshot mechanism. Version 2 enables V2 snapshot. + checkpoint.version = 1 + checkpoint.sync = true + + # Estimated number of block transactions (default 1000, min 100, max 10000). + # Total cached transactions = 65536 * txCache.estimatedTransactions + txCache.estimatedTransactions = 1000 + # If true, transaction cache initialization will be faster. + txCache.initOptimization = false + + # Number of blocks flushed to db in each batch during node syncing. + snapshot.maxFlushCount = 1 + + # Database backup settings (RocksDB only) + backup = { + enable = false + propPath = "prop.properties" + bak1path = "bak1/database/" + bak2path = "bak2/database/" + frequency = 10000 + } + + # Data root setting, for check data, currently only reward-vi is used. + # merkleRoot = { + # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net + # } +} + +node.discovery = { + enable = false + persist = false + external.ip = "" +} + +# Custom stop condition +# node.shutdown = { +# BlockTime = "54 59 08 * * ?" # if block header time in persistent db matched +# BlockHeight = 33350800 # if block header height in persistent db matched +# BlockCount = 12 # block sync count after node start +# } + +node.backup { + port = 10001 + priority = 0 + keepAliveInterval = 3000 + members = [ + # "ip", + # "ip" + ] +} + +# Algorithm for generating public key from private key. Do not modify to avoid forks. +crypto { + engine = "eckey" +} + +# Energy limit block number (config key has typo "enery" preserved for backward compatibility) +enery.limit.block.num = 4727890 + +# Actuator whitelist — empty means all actuators allowed +actuator { + whitelist = [] +} + +node.metrics = { + prometheus { + enable = false + port = 9527 + } + + storageEnable = false + + influxdb { + ip = "" + port = 8086 + database = "metrics" + metricsReportInterval = 10 + } +} + +node { + # Trust node for solidity node (example: "127.0.0.1:50051"). + # Empty string here = "not configured"; Args.java bridge converts "" → null so the + # runtime behavior matches develop (trustNodeAddr is null unless user sets the key). + trustNode = "" + + # Expose extension api to public or not + walletExtensionApi = false + + listen.port = 18888 + connection.timeout = 2 + fetchBlock.timeout = 500 + + # Number of blocks to fetch in one batch during sync. Range: [100, 2000]. + syncFetchBatchNum = 2000 + + # Number of validate sign threads, default availableProcessors + # Number of validate sign threads, 0 = auto (availableProcessors) + validateSignThreadNum = 0 + + maxConnections = 30 + minConnections = 8 + minActiveConnections = 3 + maxConnectionsWithSameIp = 2 + maxHttpConnectNumber = 50 + minParticipationRate = 0 + + # Whether to enable shielded transaction API + allowShieldedTransactionApi = true + + # Whether to print config log at startup + openPrintLog = true + + # If true, SR packs transactions into a block in descending order of fee; + # otherwise, packs by receive timestamp. + openTransactionSort = false + + # Threshold for broadcast transactions received from each peer per second, + # transactions exceeding this are discarded + maxTps = 1000 + + isOpenFullTcpDisconnect = false + inactiveThreshold = 600 // seconds + tcpNettyWorkThreadNum = 0 + udpNettyWorkThreadNum = 1 + maxFastForwardNum = 4 + activeConnectFactor = 0.1 + connectFactor = 0.6 + # Legacy alias `maxActiveNodesWithSameIp` is still accepted from user config + # (see NodeConfig alias-fallback) but is intentionally NOT defaulted here — + # shipping it in reference.conf would always mask the modern `maxConnectionsWithSameIp`. + channel.read.timeout = 0 + metricsEnable = false + + p2p { + version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 + } + + active = [ + # Active establish connection in any case + # "ip:port", + # "ip:port" + ] + + passive = [ + # Passive accept connection in any case + # "ip:port", + # "ip:port" + ] + + fastForward = [ + "100.27.171.62:18888", + "15.188.6.125:18888" + ] + + http { + fullNodeEnable = true + fullNodePort = 8090 + solidityEnable = true + solidityPort = 8091 + PBFTEnable = true + PBFTPort = 8092 + } + + rpc { + enable = true + port = 50051 + solidityEnable = true + solidityPort = 50061 + PBFTEnable = true + PBFTPort = 50071 + + # Number of gRPC threads, 0 = auto (availableProcessors / 2) + thread = 0 + + # Maximum concurrent calls per incoming connection + # No limit on concurrent calls per connection + maxConcurrentCallsPerConnection = 2147483647 + + # HTTP/2 flow control window (bytes), default 1MB + flowControlWindow = 1048576 + + # Connection idle timeout (ms). No limit by default. + maxConnectionIdleInMillis = 9223372036854775807 + + # Connection max age (ms). No limit by default. + maxConnectionAgeInMillis = 9223372036854775807 + + # Maximum message size (bytes), default 4MB + maxMessageSize = 4194304 + + # Maximum header list size (bytes), default 8192 + maxHeaderListSize = 8192 + + # RST_STREAM frames allowed per connection per period, 0 = no limit + maxRstStream = 0 + + # Seconds per period for gRPC RST_STREAM limit + secondsPerWindow = 0 + + # Minimum effective connections required to broadcast transactions + minEffectiveConnection = 1 + + # Reflection service switch for grpcurl tool + reflectionService = false + trxCacheEnable = false + } + + # Number of solidity threads in FullNode. + # Increase if solidity rpc/http interface timeouts occur. + # Default: number of cpu cores. + # Number of solidity threads, 0 = auto (availableProcessors) + solidity.threads = 0 + + # Maximum percentage of producing block interval (provides time for broadcast etc.) + blockProducedTimeOut = 50 + + # Maximum transactions from network layer per second + netMaxTrxPerSecond = 700 + + # Whether to enable node detection function + nodeDetectEnable = false + + # Use IPv6 address for node discovery and TCP connection + enableIpv6 = false + + # If node's highest block is below all peers, try to acquire new connection + effectiveCheckEnable = false + + # Dynamic loading configuration function + dynamicConfig = { + enable = false + checkInterval = 600 + } + + # Block solidification check + unsolidifiedBlockCheck = false + maxUnsolidifiedBlocks = 54 + blockCacheTimeout = 60 + + # TCP and transaction limits + receiveTcpMinDataLength = 2048 + maxTransactionPendingSize = 2000 + pendingTransactionTimeout = 60000 + + # Consensus agreement + agreeNodeCount = 0 + + # Shielded transaction (ZK) + zenTokenId = "000000" + shieldedTransInPendingMaxCounts = 10 + + # Contract proto validation thread pool (0 = auto: availableProcessors) + validContractProto.threads = 0 + + dns { + treeUrls = [ + # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", + ] + publish = false + dnsDomain = "" + dnsPrivate = "" + knownUrls = [] + staticNodes = [] + maxMergeSize = 0 + changeThreshold = 0.0 + serverType = "" + accessKeyId = "" + accessKeySecret = "" + aliyunDnsEndpoint = "" + awsRegion = "" + awsHostZoneId = "" + } + + # Open history query APIs on lite FullNode (may return null for some queries) + openHistoryQueryWhenLiteFN = false + + jsonrpc { + httpFullNodeEnable = false + httpFullNodePort = 8545 + httpSolidityEnable = false + httpSolidityPort = 8555 + httpPBFTEnable = false + httpPBFTPort = 8565 + + # Maximum blocks range for eth_getLogs, >0 otherwise no limit + maxBlockRange = 5000 + + # Maximum topics within a topic criteria, >0 otherwise no limit + maxSubTopics = 1000 + + # Maximum number for blockFilter + maxBlockFilterNum = 50000 + } + + # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. + disabledApi = [ + # "getaccount", + # "getnowblock2" + ] +} + +## Rate limiter config +rate.limiter = { + # Strategies: GlobalPreemptibleAdapter, QpsRateLimiterAdapter, IPQPSRateLimiterAdapter + # Default: QpsRateLimiterAdapter with qps=1000 + + http = [ + # { + # component = "GetNowBlockServlet", + # strategy = "GlobalPreemptibleAdapter", + # paramString = "permit=1" + # }, + # { + # component = "GetAccountServlet", + # strategy = "IPQPSRateLimiterAdapter", + # paramString = "qps=1" + # }, + # { + # component = "ListWitnessesServlet", + # strategy = "QpsRateLimiterAdapter", + # paramString = "qps=1" + # } + ] + + rpc = [ + # { + # component = "protocol.Wallet/GetBlockByLatestNum2", + # strategy = "GlobalPreemptibleAdapter", + # paramString = "permit=1" + # }, + # { + # component = "protocol.Wallet/GetAccount", + # strategy = "IPQPSRateLimiterAdapter", + # paramString = "qps=1" + # }, + # { + # component = "protocol.Wallet/ListWitnesses", + # strategy = "QpsRateLimiterAdapter", + # paramString = "qps=1" + # } + ] + + p2p = { + syncBlockChain = 3.0 + fetchInvData = 3.0 + disconnect = 1.0 + } + + global.qps = 50000 + global.ip.qps = 10000 + global.api.qps = 1000 +} + +seed.node = { + ip.list = [ + "3.225.171.164:18888", + "52.8.46.215:18888", + "3.79.71.167:18888", + "108.128.110.16:18888", + "18.133.82.227:18888", + "35.180.81.133:18888", + "13.210.151.5:18888", + "18.231.27.82:18888", + "3.12.212.122:18888", + "52.24.128.7:18888", + "15.207.144.3:18888", + "3.39.38.55:18888", + "54.151.226.240:18888", + "35.174.93.198:18888", + "18.210.241.149:18888", + "54.177.115.127:18888", + "54.254.131.82:18888", + "18.167.171.167:18888", + "54.167.11.177:18888", + "35.74.7.196:18888", + "52.196.244.176:18888", + "54.248.129.19:18888", + "43.198.142.160:18888", + "3.0.214.7:18888", + "54.153.59.116:18888", + "54.153.94.160:18888", + "54.82.161.39:18888", + "54.179.207.68:18888", + "18.142.82.44:18888", + "18.163.230.203:18888", + # "[2a05:d014:1f2f:2600:1b15:921:d60b:4c60]:18888", // use this if support ipv6 + # "[2600:1f18:7260:f400:8947:ebf3:78a0:282b]:18888", // use this if support ipv6 + ] +} + +genesis.block = { + assets = [ + { + accountName = "Zion" + accountType = "AssetIssue" + address = "TLLM21wteSPs4hKjbxgmH1L6poyMjeTbHm" + balance = "99000000000000000" + }, + { + accountName = "Sun" + accountType = "AssetIssue" + address = "TXmVpin5vq5gdZsciyyjdZgKRUju4st1wM" + balance = "0" + }, + { + accountName = "Blackhole" + accountType = "AssetIssue" + address = "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" + balance = "-9223372036854775808" + } + ] + + witnesses = [ + { + address: THKJYuUmMKKARNf7s2VT51g5uPY6KEqnat, + url = "http://GR1.com", + voteCount = 100000026 + }, + { + address: TVDmPWGYxgi5DNeW8hXrzrhY8Y6zgxPNg4, + url = "http://GR2.com", + voteCount = 100000025 + }, + { + address: TWKZN1JJPFydd5rMgMCV5aZTSiwmoksSZv, + url = "http://GR3.com", + voteCount = 100000024 + }, + { + address: TDarXEG2rAD57oa7JTK785Yb2Et32UzY32, + url = "http://GR4.com", + voteCount = 100000023 + }, + { + address: TAmFfS4Tmm8yKeoqZN8x51ASwdQBdnVizt, + url = "http://GR5.com", + voteCount = 100000022 + }, + { + address: TK6V5Pw2UWQWpySnZyCDZaAvu1y48oRgXN, + url = "http://GR6.com", + voteCount = 100000021 + }, + { + address: TGqFJPFiEqdZx52ZR4QcKHz4Zr3QXA24VL, + url = "http://GR7.com", + voteCount = 100000020 + }, + { + address: TC1ZCj9Ne3j5v3TLx5ZCDLD55MU9g3XqQW, + url = "http://GR8.com", + voteCount = 100000019 + }, + { + address: TWm3id3mrQ42guf7c4oVpYExyTYnEGy3JL, + url = "http://GR9.com", + voteCount = 100000018 + }, + { + address: TCvwc3FV3ssq2rD82rMmjhT4PVXYTsFcKV, + url = "http://GR10.com", + voteCount = 100000017 + }, + { + address: TFuC2Qge4GxA2U9abKxk1pw3YZvGM5XRir, + url = "http://GR11.com", + voteCount = 100000016 + }, + { + address: TNGoca1VHC6Y5Jd2B1VFpFEhizVk92Rz85, + url = "http://GR12.com", + voteCount = 100000015 + }, + { + address: TLCjmH6SqGK8twZ9XrBDWpBbfyvEXihhNS, + url = "http://GR13.com", + voteCount = 100000014 + }, + { + address: TEEzguTtCihbRPfjf1CvW8Euxz1kKuvtR9, + url = "http://GR14.com", + voteCount = 100000013 + }, + { + address: TZHvwiw9cehbMxrtTbmAexm9oPo4eFFvLS, + url = "http://GR15.com", + voteCount = 100000012 + }, + { + address: TGK6iAKgBmHeQyp5hn3imB71EDnFPkXiPR, + url = "http://GR16.com", + voteCount = 100000011 + }, + { + address: TLaqfGrxZ3dykAFps7M2B4gETTX1yixPgN, + url = "http://GR17.com", + voteCount = 100000010 + }, + { + address: TX3ZceVew6yLC5hWTXnjrUFtiFfUDGKGty, + url = "http://GR18.com", + voteCount = 100000009 + }, + { + address: TYednHaV9zXpnPchSywVpnseQxY9Pxw4do, + url = "http://GR19.com", + voteCount = 100000008 + }, + { + address: TCf5cqLffPccEY7hcsabiFnMfdipfyryvr, + url = "http://GR20.com", + voteCount = 100000007 + }, + { + address: TAa14iLEKPAetX49mzaxZmH6saRxcX7dT5, + url = "http://GR21.com", + voteCount = 100000006 + }, + { + address: TBYsHxDmFaRmfCF3jZNmgeJE8sDnTNKHbz, + url = "http://GR22.com", + voteCount = 100000005 + }, + { + address: TEVAq8dmSQyTYK7uP1ZnZpa6MBVR83GsV6, + url = "http://GR23.com", + voteCount = 100000004 + }, + { + address: TRKJzrZxN34YyB8aBqqPDt7g4fv6sieemz, + url = "http://GR24.com", + voteCount = 100000003 + }, + { + address: TRMP6SKeFUt5NtMLzJv8kdpYuHRnEGjGfe, + url = "http://GR25.com", + voteCount = 100000002 + }, + { + address: TDbNE1VajxjpgM5p7FyGNDASt3UVoFbiD3, + url = "http://GR26.com", + voteCount = 100000001 + }, + { + address: TLTDZBcPoJ8tZ6TTEeEqEvwYFk2wgotSfD, + url = "http://GR27.com", + voteCount = 100000000 + } + ] + + timestamp = "0" + + parentHash = "0xe58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f" +} + +# Optional. Used when the witness account has set witnessPermission. +# localWitnessAccountAddress = + +localwitness = [ +] + +# localwitnesskeystore = [ +# "localwitnesskeystore.json" +# ] + +block = { + needSyncCheck = false + maintenanceTimeInterval = 21600000 // 6 hours (ms) + proposalExpireTime = 259200000 // 3 days (ms), controlled by committee proposal + checkFrozenTime = 1 // maintenance periods to check frozen balance (test only) +} + +# Transaction reference block: "solid" or "head". Default "solid". "head" may cause TaPos error. +trx.reference.block = "solid" + +# Transaction expiration time in milliseconds. +trx.expiration.timeInMilliseconds = 60000 + +vm = { + supportConstant = false + maxEnergyLimitForConstant = 100000000 + minTimeRatio = 0.0 + maxTimeRatio = 5.0 + saveInternalTx = false + lruCacheSize = 500 + vmTrace = false + + # Whether to store featured internal transactions (freeze, vote, etc.) + saveFeaturedInternalTx = false + + # Whether to store details of CANCELALLUNFREEZEV2 opcode internal transactions + saveCancelAllUnfreezeV2Details = false + + # Max execution time (ms) for re-executed transactions during packaging + longRunningTime = 10 + + # Whether to support estimate energy API + estimateEnergy = false + + # Max retry time for executing transaction in estimating energy + estimateEnergyMaxRetry = 3 +} + +# Governance proposal toggle parameters. All default to 0 (disabled). +# Controlled by on-chain committee proposals, not manual configuration. +# Setting them in config is only for private chain testing. +committee = { + allowCreationOfContracts = 0 + allowMultiSign = 0 + allowAdaptiveEnergy = 0 + allowDelegateResource = 0 + allowSameTokenName = 0 + allowTvmTransferTrc10 = 0 + allowTvmConstantinople = 0 + allowTvmSolidity059 = 0 + forbidTransferToContract = 0 + allowShieldedTRC20Transaction = 0 + allowTvmIstanbul = 0 + allowMarketTransaction = 0 + allowProtoFilterNum = 0 + allowAccountStateRoot = 0 + changedDelegation = 0 + allowPBFT = 0 + pBFTExpireNum = 20 + allowTransactionFeePool = 0 + allowBlackHoleOptimization = 0 + allowNewResourceModel = 0 + allowReceiptsMerkleRoot = 0 + allowTvmFreeze = 0 + allowTvmVote = 0 + unfreezeDelayDays = 0 + allowTvmLondon = 0 + allowTvmCompatibleEvm = 0 + allowHigherLimitForMaxCpuTimeOfOneTx = 0 + allowNewRewardAlgorithm = 0 + allowOptimizedReturnValueOfChainId = 0 + allowTvmShangHai = 0 + allowOldRewardOpt = 0 + allowEnergyAdjustment = 0 + allowStrictMath = 0 + consensusLogicOptimization = 0 + allowTvmCancun = 0 + allowTvmBlob = 0 + allowTvmOsaka = 0 + allowAccountAssetOptimization = 0 + allowAssetOptimization = 0 + allowNewReward = 0 + memoFee = 0 + allowDelegateOptimization = 0 + allowDynamicEnergy = 0 + dynamicEnergyThreshold = 0 + dynamicEnergyIncreaseFactor = 0 + dynamicEnergyMaxFactor = 0 +} + +event.subscribe = { + enable = false + + native = { + useNativeQueue = true + bindport = 5555 + sendqueuelength = 1000 + } + + version = 0 + startSyncBlockNum = 0 + path = "" + server = "" + dbconfig = "" + contractParse = true + + topics = [ + { + triggerName = "block" + enable = false + topic = "block" + solidified = false + }, + { + triggerName = "transaction" + enable = false + topic = "transaction" + solidified = false + ethCompatible = false + }, + { + triggerName = "contractevent" + enable = false + topic = "contractevent" + }, + { + triggerName = "contractlog" + enable = false + topic = "contractlog" + redundancy = false + }, + { + triggerName = "solidity" + enable = true + topic = "solidity" + }, + { + triggerName = "solidityevent" + enable = false + topic = "solidityevent" + }, + { + triggerName = "soliditylog" + enable = false + topic = "soliditylog" + redundancy = false + } + ] + + filter = { + fromblock = "" + toblock = "" + contractAddress = [ + "" + ] + contractTopic = [ + "" + ] + } +} diff --git a/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java b/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java new file mode 100644 index 00000000000..14645242851 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java @@ -0,0 +1,56 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.core.exception.TronError; + +public class BlockConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + BlockConfig bc = BlockConfig.fromConfig(withRef()); + assertEquals(21600000L, bc.getMaintenanceTimeInterval()); + assertEquals(1, bc.getCheckFrozenTime()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "block { needSyncCheck = true, maintenanceTimeInterval = 10000," + + " checkFrozenTime = 5, proposalExpireTime = 300000 }"); + BlockConfig bc = BlockConfig.fromConfig(config); + assertEquals(true, bc.isNeedSyncCheck()); + assertEquals(10000L, bc.getMaintenanceTimeInterval()); + assertEquals(5, bc.getCheckFrozenTime()); + assertEquals(300000L, bc.getProposalExpireTime()); + } + + @Test(expected = TronError.class) + public void testProposalExpireTimeTooLow() { + BlockConfig.fromConfig(withRef("block { proposalExpireTime = 0 }")); + } + + @Test(expected = TronError.class) + public void testProposalExpireTimeTooHigh() { + BlockConfig.fromConfig(withRef("block { proposalExpireTime = 999999999999 }")); + } + + @Test(expected = TronError.class) + public void testRejectsCommitteeProposalExpireTime() { + BlockConfig.fromConfig(withRef( + "committee { proposalExpireTime = 300000 }\n" + + "block { proposalExpireTime = 300000 }")); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java new file mode 100644 index 00000000000..962b6a349ab --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java @@ -0,0 +1,233 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class CommitteeConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + CommitteeConfig cc = CommitteeConfig.fromConfig(withRef()); + assertEquals(0, cc.getAllowCreationOfContracts()); + assertEquals(0, cc.getAllowPBFT()); + assertEquals(20, cc.getPBFTExpireNum()); + assertEquals(0, cc.getUnfreezeDelayDays()); + assertEquals(0, cc.getAllowDynamicEnergy()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "committee { allowCreationOfContracts = 1, allowPBFT = 1, pBFTExpireNum = 30 }"); + CommitteeConfig cc = CommitteeConfig.fromConfig(config); + assertEquals(1, cc.getAllowCreationOfContracts()); + assertEquals(1, cc.getAllowPBFT()); + assertEquals(30, cc.getPBFTExpireNum()); + } + + @Test + public void testUnfreezeDelayDaysClamped() { + assertEquals(365, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 500 }")).getUnfreezeDelayDays()); + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = -10 }")).getUnfreezeDelayDays()); + } + + @Test + public void testDynamicEnergyClamped() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowDynamicEnergy = 5 }")).getAllowDynamicEnergy()); + } + + @Test + public void testDynamicEnergyThresholdClamped() { + assertEquals(100_000_000_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyThreshold = 999999999999999999 }")) + .getDynamicEnergyThreshold()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAllowOldRewardOptWithoutPrerequisites() { + CommitteeConfig.fromConfig(withRef("committee { allowOldRewardOpt = 1 }")); + } + + @Test + public void testAllowOldRewardOptWithPrerequisite() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowTvmVote = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // + // Background: PR #6615 (config bean refactor) silently dropped clamps for + // memoFee and allowNewReward because no test covered the boundary cases. + // These tests pin every clamp in CommitteeConfig.postProcess() so future + // refactors cannot drop them undetected. + // =========================================================================== + + // ----- memoFee: clamped to [0, 1_000_000_000] ----- + + @Test + public void testMemoFeeClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { memoFee = -100 }")).getMemoFee()); + } + + @Test + public void testMemoFeeClampedAboveMax() { + assertEquals(1_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 5000000000 }")).getMemoFee()); + } + + @Test + public void testMemoFeeInRangeUnchanged() { + assertEquals(500_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 500000000 }")).getMemoFee()); + } + + @Test + public void testMemoFeeBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 0 }")).getMemoFee()); + assertEquals(1_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 1000000000 }")).getMemoFee()); + } + + // ----- allowNewReward: clamped to [0, 1] ----- + + @Test + public void testAllowNewRewardClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = -5 }")).getAllowNewReward()); + } + + @Test + public void testAllowNewRewardClampedAboveOne() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 99 }")).getAllowNewReward()); + } + + @Test + public void testAllowNewRewardBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 0 }")).getAllowNewReward()); + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 1 }")).getAllowNewReward()); + } + + // Critical: clamp must run BEFORE the cross-field check, otherwise + // `allowNewReward = 2` (intended as "enabled") would still satisfy + // `allowNewReward != 1` and the cross-field check would throw. + // This test pins the clamp ordering. + @Test + public void testAllowNewRewardClampRunsBeforeCrossFieldCheck() { + CommitteeConfig cc = CommitteeConfig.fromConfig(withRef( + "committee { allowOldRewardOpt = 1, allowNewReward = 2 }")); + assertEquals(1, cc.getAllowNewReward()); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + // ----- allowDelegateOptimization: clamped to [0, 1] ----- + + @Test + public void testAllowDelegateOptimizationClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowDelegateOptimization = -3 }")) + .getAllowDelegateOptimization()); + } + + @Test + public void testAllowDelegateOptimizationClampedAboveOne() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowDelegateOptimization = 7 }")) + .getAllowDelegateOptimization()); + } + + // ----- allowDynamicEnergy: clamped to [0, 1] ----- + + @Test + public void testAllowDynamicEnergyClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowDynamicEnergy = -1 }")).getAllowDynamicEnergy()); + } + + // ----- unfreezeDelayDays: clamped to [0, 365] (boundary values) ----- + + @Test + public void testUnfreezeDelayDaysBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 0 }")).getUnfreezeDelayDays()); + assertEquals(365, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 365 }")).getUnfreezeDelayDays()); + assertEquals(100, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 100 }")).getUnfreezeDelayDays()); + } + + // ----- dynamicEnergyThreshold: clamped to [0, 100_000_000_000_000_000] ----- + + @Test + public void testDynamicEnergyThresholdClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyThreshold = -1 }")) + .getDynamicEnergyThreshold()); + } + + // ----- dynamicEnergyIncreaseFactor: clamped to [0, 10_000] ----- + + @Test + public void testDynamicEnergyIncreaseFactorClamped() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = -1 }")) + .getDynamicEnergyIncreaseFactor()); + assertEquals(10_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = 10001 }")) + .getDynamicEnergyIncreaseFactor()); + assertEquals(5_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = 5000 }")) + .getDynamicEnergyIncreaseFactor()); + } + + // ----- dynamicEnergyMaxFactor: clamped to [0, 100_000] ----- + + @Test + public void testDynamicEnergyMaxFactorClamped() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = -1 }")) + .getDynamicEnergyMaxFactor()); + assertEquals(100_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = 100001 }")) + .getDynamicEnergyMaxFactor()); + assertEquals(50_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = 50000 }")) + .getDynamicEnergyMaxFactor()); + } + + // ----- Cross-field validation for allowOldRewardOpt ----- + + @Test + public void testAllowOldRewardOptWithAllowNewReward() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowNewReward = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + @Test + public void testAllowOldRewardOptWithAllowNewRewardAlgorithm() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowNewRewardAlgorithm = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java new file mode 100644 index 00000000000..361d9f48581 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -0,0 +1,82 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class EventConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + EventConfig ec = EventConfig.fromConfig(empty); + // reference.conf has event.subscribe with enable=false, topics with 7 entries + assertFalse(ec.isEnable()); + assertEquals(0, ec.getVersion()); + assertEquals("", ec.getPath()); + assertFalse(ec.getTopics().isEmpty()); // reference.conf has default topic entries + } + + @Test + public void testNativeQueue() { + Config config = withRef( + "event.subscribe { enable = true," + + " native { useNativeQueue = true, bindport = 6666, sendqueuelength = 2000 } }"); + EventConfig ec = EventConfig.fromConfig(config); + assertTrue(ec.isEnable()); + assertTrue(ec.getNativeQueue().isUseNativeQueue()); + assertEquals(6666, ec.getNativeQueue().getBindport()); + assertEquals(2000, ec.getNativeQueue().getSendqueuelength()); + } + + @Test + public void testTopicsWithOptionalFields() { + Config config = withRef( + "event.subscribe { enable = true, topics = [" + + "{ triggerName = block, enable = true, topic = block }," + + "{ triggerName = transaction, enable = false, topic = tx," + + " ethCompatible = true, solidified = true, redundancy = true }" + + "] }"); + EventConfig ec = EventConfig.fromConfig(config); + assertEquals(2, ec.getTopics().size()); + + EventConfig.TopicConfig t1 = ec.getTopics().get(0); + assertEquals("block", t1.getTriggerName()); + assertTrue(t1.isEnable()); + assertFalse(t1.isEthCompatible()); // not set, default false + assertFalse(t1.isSolidified()); + assertFalse(t1.isRedundancy()); + + EventConfig.TopicConfig t2 = ec.getTopics().get(1); + assertEquals("transaction", t2.getTriggerName()); + assertTrue(t2.isEthCompatible()); + assertTrue(t2.isSolidified()); + assertTrue(t2.isRedundancy()); + } + + @Test + public void testFilter() { + Config config = withRef( + "event.subscribe { enable = true," + + " filter { fromblock = \"100\", toblock = \"200\"," + + " contractAddress = [\"addr1\", \"addr2\"]," + + " contractTopic = [\"topic1\"] } }"); + EventConfig ec = EventConfig.fromConfig(config); + assertEquals("100", ec.getFilter().getFromblock()); + assertEquals("200", ec.getFilter().getToblock()); + assertEquals(2, ec.getFilter().getContractAddress().size()); + assertEquals(1, ec.getFilter().getContractTopic().size()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java new file mode 100644 index 00000000000..5e653a79b7f --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java @@ -0,0 +1,59 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class GenesisConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + GenesisConfig gc = GenesisConfig.fromConfig(empty); + // reference.conf has genesis.block with timestamp, parentHash, assets, witnesses + assertEquals("0", gc.getTimestamp()); + assertFalse(gc.getAssets().isEmpty()); // reference.conf has seed accounts + assertFalse(gc.getWitnesses().isEmpty()); // reference.conf has seed witnesses + } + + @Test + public void testWithAssets() { + Config config = withRef( + "genesis.block { timestamp = \"12345\", parentHash = \"0x00\"," + + " assets = [{ accountName = Zion, accountType = AssetIssue," + + " address = \"TAddr1\", balance = \"99000\" }]," + + " witnesses = [{ address = \"TWitness1\", url = \"http://test.com\"," + + " voteCount = 100 }] }"); + GenesisConfig gc = GenesisConfig.fromConfig(config); + assertEquals("12345", gc.getTimestamp()); + assertEquals("0x00", gc.getParentHash()); + assertEquals(1, gc.getAssets().size()); + assertEquals("Zion", gc.getAssets().get(0).getAccountName()); + assertEquals("TAddr1", gc.getAssets().get(0).getAddress()); + assertEquals(1, gc.getWitnesses().size()); + assertEquals("TWitness1", gc.getWitnesses().get(0).getAddress()); + assertEquals(100, gc.getWitnesses().get(0).getVoteCount()); + } + + @Test + public void testEmptyLists() { + Config config = withRef( + "genesis.block { timestamp = \"0\", parentHash = \"0x00\"," + + " assets = [], witnesses = [] }"); + GenesisConfig gc = GenesisConfig.fromConfig(config); + assertTrue(gc.getAssets().isEmpty()); + assertTrue(gc.getWitnesses().isEmpty()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java b/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java new file mode 100644 index 00000000000..0c163ef31f7 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java @@ -0,0 +1,48 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class LocalWitnessConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(empty); + assertTrue(lw.getPrivateKeys().isEmpty()); + assertNull(lw.getAccountAddress()); + assertTrue(lw.getKeystores().isEmpty()); + } + + @Test + public void testWithPrivateKeys() { + Config config = withRef( + "localwitness = [\"key1\", \"key2\"]\n" + + "localWitnessAccountAddress = \"TAddr123\""); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(config); + assertEquals(2, lw.getPrivateKeys().size()); + assertEquals("key1", lw.getPrivateKeys().get(0)); + assertEquals("TAddr123", lw.getAccountAddress()); + } + + @Test + public void testWithKeystores() { + Config config = withRef( + "localwitnesskeystore = [\"/path/to/keystore1\"]"); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(config); + assertEquals(1, lw.getKeystores().size()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java b/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java new file mode 100644 index 00000000000..b641e4d1924 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java @@ -0,0 +1,48 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class MetricsConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + MetricsConfig mc = MetricsConfig.fromConfig(empty); + assertFalse(mc.isStorageEnable()); + assertFalse(mc.getPrometheus().isEnable()); + assertEquals(9527, mc.getPrometheus().getPort()); + assertEquals(8086, mc.getInfluxdb().getPort()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "node.metrics {" + + " storageEnable = true," + + " prometheus { enable = true, port = 9999 }," + + " influxdb { ip = \"10.0.0.1\", port = 9086, database = mydb," + + " metricsReportInterval = 30 } }"); + MetricsConfig mc = MetricsConfig.fromConfig(config); + assertTrue(mc.isStorageEnable()); + assertTrue(mc.getPrometheus().isEnable()); + assertEquals(9999, mc.getPrometheus().getPort()); + assertEquals("10.0.0.1", mc.getInfluxdb().getIp()); + assertEquals(9086, mc.getInfluxdb().getPort()); + assertEquals("mydb", mc.getInfluxdb().getDatabase()); + assertEquals(30, mc.getInfluxdb().getMetricsReportInterval()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java new file mode 100644 index 00000000000..ed369d6c35f --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java @@ -0,0 +1,56 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.core.Constant; + +public class MiscConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + MiscConfig mc = MiscConfig.fromConfig(empty); + assertTrue(mc.isNeedToUpdateAsset()); + assertFalse(mc.isHistoryBalanceLookup()); + assertEquals("solid", mc.getTrxReferenceBlock()); + assertEquals(Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME, + mc.getTrxExpirationTimeInMilliseconds()); + // reference.conf has crypto.engine = "eckey" (lowercase) + assertEquals("eckey", mc.getCryptoEngine()); + // reference.conf has seed.node.ip.list with actual IPs + assertFalse(mc.getSeedNodeIpList().isEmpty()); + assertTrue(mc.getActuatorWhitelist().isEmpty()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "storage { needToUpdateAsset = false," + + " balance { history { lookup = true } } }\n" + + "trx { reference { block = head } }\n" + + "crypto { engine = sm2 }\n" + + "seed.node { ip.list = [\"1.2.3.4:18888\"] }\n" + + "actuator { whitelist = [\"CreateSmartContract\"] }"); + MiscConfig mc = MiscConfig.fromConfig(config); + assertFalse(mc.isNeedToUpdateAsset()); + assertTrue(mc.isHistoryBalanceLookup()); + assertEquals("head", mc.getTrxReferenceBlock()); + assertEquals("sm2", mc.getCryptoEngine()); + assertEquals(1, mc.getSeedNodeIpList().size()); + assertEquals(1, mc.getActuatorWhitelist().size()); + assertTrue(mc.getActuatorWhitelist().contains("CreateSmartContract")); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java new file mode 100644 index 00000000000..fb22029262e --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -0,0 +1,319 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class NodeConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + NodeConfig nc = NodeConfig.fromConfig(empty); + assertEquals(18888, nc.getListenPort()); + assertEquals(2, nc.getConnectionTimeout()); + assertEquals(500, nc.getFetchBlockTimeout()); + assertEquals(30, nc.getMaxConnections()); + assertEquals(8, nc.getMinConnections()); + assertEquals(4, nc.getMaxFastForwardNum()); + assertFalse(nc.isOpenFullTcpDisconnect()); + // reference.conf matches code default: discovery disabled when not configured + assertFalse(nc.isDiscoveryEnable()); + assertFalse(nc.isDiscoveryPersist()); + assertEquals(0, nc.getChannelReadTimeout()); + } + + @Test + public void testDotNotationFields() { + Config config = withRef( + "node { listen { port = 19999 }, connection { timeout = 5 }," + + " fetchBlock { timeout = 300 }, solidity { threads = 4 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(19999, nc.getListenPort()); + assertEquals(5, nc.getConnectionTimeout()); + assertEquals(300, nc.getFetchBlockTimeout()); + assertEquals(4, nc.getSolidityThreads()); + } + + @Test + public void testDiscoveryFields() { + Config config = withRef( + "node.discovery { enable = true, persist = true }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertTrue(nc.isDiscoveryEnable()); + assertTrue(nc.isDiscoveryPersist()); + } + + @Test + public void testHttpSubBean() { + Config config = withRef( + "node { http { fullNodeEnable = false, fullNodePort = 9090," + + " PBFTEnable = false, PBFTPort = 9092 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertFalse(nc.getHttp().isFullNodeEnable()); + assertEquals(9090, nc.getHttp().getFullNodePort()); + assertFalse(nc.getHttp().isPBFTEnable()); + assertEquals(9092, nc.getHttp().getPBFTPort()); + } + + @Test + public void testRpcSubBean() { + Config config = withRef( + "node { rpc { enable = false, port = 60051," + + " PBFTEnable = false, PBFTPort = 60071 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertFalse(nc.getRpc().isEnable()); + assertEquals(60051, nc.getRpc().getPort()); + assertFalse(nc.getRpc().isPBFTEnable()); + assertEquals(60071, nc.getRpc().getPBFTPort()); + } + + @Test + public void testBackupSubBean() { + Config config = withRef( + "node { backup { priority = 5, port = 20001, keepAliveInterval = 5000 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(5, nc.getBackup().getPriority()); + assertEquals(20001, nc.getBackup().getPort()); + assertEquals(5000, nc.getBackup().getKeepAliveInterval()); + } + + @Test + public void testIsOpenFullTcpDisconnect() { + Config config = withRef( + "node { isOpenFullTcpDisconnect = true }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertTrue(nc.isOpenFullTcpDisconnect()); + } + + @Test + public void testRpcDefaultsFromReference() { + Config empty = withRef(); + NodeConfig nc = NodeConfig.fromConfig(empty); + NodeConfig.RpcConfig rpc = nc.getRpc(); + + // reference.conf provides actual final defaults, no sentinel conversion needed + assertEquals(2147483647, rpc.getMaxConcurrentCallsPerConnection()); + assertEquals(1048576, rpc.getFlowControlWindow()); + assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis()); + assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis()); + assertEquals(4194304, rpc.getMaxMessageSize()); + assertEquals(8192, rpc.getMaxHeaderListSize()); + assertEquals(1, rpc.getMinEffectiveConnection()); + // thread=0 in reference.conf triggers auto-detect in postProcess + assertTrue(rpc.getThread() > 0); + } + + @Test + public void testRpcUserOverrideZeroNotConverted() { + // Users can explicitly set 0 to disable connection checks (e.g. system-test) + Config config = withRef( + "node { rpc { minEffectiveConnection = 0 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(0, nc.getRpc().getMinEffectiveConnection()); + } + + @Test + public void testRpcUserOverrideExplicitValues() { + Config config = withRef( + "node { rpc { thread = 32," + + " maxConcurrentCallsPerConnection = 50," + + " flowControlWindow = 2097152," + + " maxMessageSize = 8388608," + + " maxHeaderListSize = 16384 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + NodeConfig.RpcConfig rpc = nc.getRpc(); + assertEquals(32, rpc.getThread()); + assertEquals(50, rpc.getMaxConcurrentCallsPerConnection()); + assertEquals(2097152, rpc.getFlowControlWindow()); + assertEquals(8388608, rpc.getMaxMessageSize()); + assertEquals(16384, rpc.getMaxHeaderListSize()); + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // Pin every clamp in NodeConfig.postProcess() so future refactors cannot + // drop them undetected (regression seen in PR #6615 with CommitteeConfig). + // =========================================================================== + + // ----- blockProducedTimeOut: clamped to [30, 100] ----- + + @Test + public void testBlockProducedTimeOutClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 10 }")); + assertEquals(30, nc.getBlockProducedTimeOut()); + } + + @Test + public void testBlockProducedTimeOutClampedAboveMax() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 200 }")); + assertEquals(100, nc.getBlockProducedTimeOut()); + } + + @Test + public void testBlockProducedTimeOutBoundaryValues() { + assertEquals(30, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 30 }")).getBlockProducedTimeOut()); + assertEquals(100, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 100 }")).getBlockProducedTimeOut()); + assertEquals(75, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 75 }")).getBlockProducedTimeOut()); + } + + // ----- inactiveThreshold: minimum 1 ----- + + @Test + public void testInactiveThresholdClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 0 }")); + assertEquals(1, nc.getInactiveThreshold()); + } + + @Test + public void testInactiveThresholdClampedNegative() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { inactiveThreshold = -100 }")); + assertEquals(1, nc.getInactiveThreshold()); + } + + @Test + public void testInactiveThresholdInRangeUnchanged() { + assertEquals(1, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 1 }")).getInactiveThreshold()); + assertEquals(600, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 600 }")).getInactiveThreshold()); + assertEquals(1000, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 1000 }")).getInactiveThreshold()); + } + + // ----- maxFastForwardNum: clamped to [1, MAX_ACTIVE_WITNESS_NUM=27] ----- + + @Test + public void testMaxFastForwardNumClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 0 }")); + assertEquals(1, nc.getMaxFastForwardNum()); + } + + @Test + public void testMaxFastForwardNumClampedAboveMax() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 100 }")); + assertEquals(27, nc.getMaxFastForwardNum()); + } + + @Test + public void testMaxFastForwardNumBoundaryValues() { + assertEquals(1, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 1 }")).getMaxFastForwardNum()); + assertEquals(27, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 27 }")).getMaxFastForwardNum()); + assertEquals(4, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 4 }")).getMaxFastForwardNum()); + } + + // ----- validContractProto.threads: 0 = auto (availableProcessors) ----- + + @Test + public void testValidContractProtoThreadsDefaultAutoExpands() { + // Default in reference.conf is 0; postProcess must expand to availableProcessors. + // Matches develop Args.java:743-746 runtime fallback. + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertEquals(Runtime.getRuntime().availableProcessors(), + nc.getValidContractProtoThreads()); + } + + @Test + public void testValidContractProtoThreadsExplicitPreserved() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { validContractProto { threads = 3 } }")); + assertEquals(3, nc.getValidContractProtoThreads()); + } + + // ----- trustNode: empty reference.conf default means trustNode stays unset ----- + + @Test + public void testTrustNodeNotDefaultedByReferenceConf() { + // reference.conf intentionally omits `node.trustNode` so that empty configs + // preserve develop's behavior (trustNodeAddr stays null in the Args bridge). + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertTrue(nc.getTrustNode() == null || nc.getTrustNode().isEmpty()); + } + + // ----- maxConnectionsWithSameIp alias: reference.conf must not poison merge ----- + + @Test + public void testMaxConnectionsWithSameIpNotOverriddenByReferenceConfAlias() { + // reference.conf must NOT ship `maxActiveNodesWithSameIp`, otherwise the alias- + // fallback branch would silently clobber the user's modern key. Regression guard + // for review #2 (317787106, 2026-04-16). + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxConnectionsWithSameIp = 10 }")); + assertEquals(10, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testMaxActiveNodesWithSameIpLegacyAliasStillWorks() { + // Back-compat: users who still write the legacy key in their config.conf + // must get their value routed to maxConnectionsWithSameIp. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxActiveNodesWithSameIp = 5 }")); + assertEquals(5, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testLegacyAliasTakesPriorityOverModernKey() { + // Matches develop Args.java:392-399: if the legacy key is present, it wins. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxActiveNodesWithSameIp = 5, maxConnectionsWithSameIp = 10 }")); + assertEquals(5, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testShieldedApiDefaultsToTrueWhenNeitherKeySet() { + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertTrue(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiModernKeyRespected() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node.allowShieldedTransactionApi = false")); + assertFalse(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiLegacyKeyRespected() { + // Regression guard: reference.conf ships `allowShieldedTransactionApi = true`, which + // used to make the legacy-key fallback dead code. A user who only set the legacy key + // must still have their value honored. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node.fullNodeAllowShieldedTransaction = false")); + assertFalse(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiLegacyKeyTakesPriorityOverModern() { + // Consistent with maxActiveNodesWithSameIp: legacy key presence wins over modern. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node {\n" + + " allowShieldedTransactionApi = false\n" + + " fullNodeAllowShieldedTransaction = true\n" + + "}")); + assertTrue(nc.isAllowShieldedTransactionApi()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java new file mode 100644 index 00000000000..7b4d8a87d45 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java @@ -0,0 +1,54 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class RateLimiterConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + RateLimiterConfig rl = RateLimiterConfig.fromConfig(empty); + assertEquals(50000, rl.getGlobal().getQps()); + assertEquals(10000, rl.getGlobal().getIp().getQps()); + assertEquals(1000, rl.getGlobal().getApi().getQps()); + assertEquals(3.0, rl.getP2p().getSyncBlockChain(), 0.001); + assertEquals(3.0, rl.getP2p().getFetchInvData(), 0.001); + assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001); + assertTrue(rl.getHttp().isEmpty()); + assertTrue(rl.getRpc().isEmpty()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "rate.limiter {" + + " global { qps = 100, ip { qps = 50 }, api { qps = 10 } }," + + " p2p { syncBlockChain = 5.0, disconnect = 2.0 }," + + " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter," + + " paramString = \"qps=10\" }]," + + " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter," + + " paramString = \"permit=1\" }]" + + "}"); + RateLimiterConfig rl = RateLimiterConfig.fromConfig(config); + assertEquals(100, rl.getGlobal().getQps()); + assertEquals(50, rl.getGlobal().getIp().getQps()); + assertEquals(5.0, rl.getP2p().getSyncBlockChain(), 0.001); + assertEquals(1, rl.getHttp().size()); + assertEquals("TestServlet", rl.getHttp().get(0).getComponent()); + assertEquals(1, rl.getRpc().size()); + assertEquals("TestRpc", rl.getRpc().get(0).getComponent()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java new file mode 100644 index 00000000000..5a679be89e5 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -0,0 +1,142 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.common.math.StrictMathWrapper; + +public class StorageConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + assertEquals("LEVELDB", sc.getDb().getEngine()); + assertFalse(sc.getDb().isSync()); + assertEquals("database", sc.getDb().getDirectory()); + assertEquals("index", sc.getIndex().getDirectory()); + assertTrue(sc.isNeedToUpdateAsset()); + assertFalse(sc.getBackup().isEnable()); + assertEquals(10000, sc.getBackup().getFrequency()); + assertEquals(7, sc.getDbSettings().getLevelNumber()); + assertEquals(5000, sc.getDbSettings().getMaxOpenFiles()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "storage { db { engine = ROCKSDB, sync = true, directory = mydb }," + + " backup { enable = true, frequency = 5000 }," + + " dbSettings { levelNumber = 5, maxOpenFiles = 3000 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals("ROCKSDB", sc.getDb().getEngine()); + assertTrue(sc.getDb().isSync()); + assertEquals("mydb", sc.getDb().getDirectory()); + assertTrue(sc.getBackup().isEnable()); + assertEquals(5000, sc.getBackup().getFrequency()); + assertEquals(5, sc.getDbSettings().getLevelNumber()); + assertEquals(3000, sc.getDbSettings().getMaxOpenFiles()); + } + + @Test + public void testCheckpointDefaults() { + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + assertEquals(1, sc.getCheckpoint().getVersion()); + assertTrue(sc.getCheckpoint().isSync()); + } + + @Test + public void testDbSettingsDefaults() { + // These defaults must match develop's Args.initRocksDbSettings() fallbacks so that + // nodes with minimal configs retain the same RocksDB tuning. See + // docs/plans/2026-04-21-001-fix-reference-conf-default-drift.md. + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + StorageConfig.DbSettingsConfig ds = sc.getDbSettings(); + assertEquals(7, ds.getLevelNumber()); + // compactThreads default is 0 in reference.conf, auto-expanded by postProcess() + assertEquals(StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1), + ds.getCompactThreads()); + assertEquals(16, ds.getBlocksize()); + assertEquals(256, ds.getMaxBytesForLevelBase()); + assertEquals(10, ds.getMaxBytesForLevelMultiplier(), 0.01); + assertEquals(2, ds.getLevel0FileNumCompactionTrigger()); + assertEquals(64, ds.getTargetFileSizeBase()); + assertEquals(1, ds.getTargetFileSizeMultiplier()); + assertEquals(5000, ds.getMaxOpenFiles()); + } + + @Test + public void testCompactThreadsAutoExpand() { + // compactThreads = 0 must be auto-expanded to availableProcessors (min 1) + Config config = withRef("storage { dbSettings { compactThreads = 0 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals(StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1), + sc.getDbSettings().getCompactThreads()); + } + + @Test + public void testCompactThreadsExplicitPreserved() { + // Non-zero compactThreads must be passed through untouched + Config config = withRef("storage { dbSettings { compactThreads = 7 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals(7, sc.getDbSettings().getCompactThreads()); + } + + @Test + public void testBalanceHistoryLookup() { + Config config = withRef( + "storage { balance { history { lookup = true } } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertTrue(sc.getBalance().getHistory().isLookup()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountZeroRejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = 0")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountNegativeRejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = -1")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountOver500Rejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = 501")); + } + + @Test + public void testTxCacheEstimatedClampedBelowMin() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 50")); + assertEquals(100, sc.getTxCache().getEstimatedTransactions()); + } + + @Test + public void testTxCacheEstimatedClampedAboveMax() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 99999")); + assertEquals(10000, sc.getTxCache().getEstimatedTransactions()); + } + + @Test + public void testTxCacheEstimatedWithinRangePreserved() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 5000")); + assertEquals(5000, sc.getTxCache().getEstimatedTransactions()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java new file mode 100644 index 00000000000..b134fe00c2b --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -0,0 +1,91 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class VmConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + VmConfig vm = VmConfig.fromConfig(empty); + assertFalse(vm.isSupportConstant()); + assertEquals(100_000_000L, vm.getMaxEnergyLimitForConstant()); + assertEquals(500, vm.getLruCacheSize()); + assertEquals(0.0, vm.getMinTimeRatio(), 0.001); + assertEquals(5.0, vm.getMaxTimeRatio(), 0.001); + assertEquals(10, vm.getLongRunningTime()); + assertFalse(vm.isEstimateEnergy()); + assertEquals(3, vm.getEstimateEnergyMaxRetry()); + assertFalse(vm.isVmTrace()); + assertFalse(vm.isSaveInternalTx()); + assertFalse(vm.isSaveFeaturedInternalTx()); + assertFalse(vm.isSaveCancelAllUnfreezeV2Details()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "vm { supportConstant = true, lruCacheSize = 1000, minTimeRatio = 0.5 }"); + VmConfig vm = VmConfig.fromConfig(config); + assertTrue(vm.isSupportConstant()); + assertEquals(1000, vm.getLruCacheSize()); + assertEquals(0.5, vm.getMinTimeRatio(), 0.001); + } + + @Test + public void testMaxEnergyLimitClamped() { + Config config = withRef("vm { maxEnergyLimitForConstant = 100 }"); + VmConfig vm = VmConfig.fromConfig(config); + assertEquals(3_000_000L, vm.getMaxEnergyLimitForConstant()); + } + + @Test + public void testEstimateEnergyMaxRetryClamped() { + Config tooHigh = withRef("vm { estimateEnergyMaxRetry = 50 }"); + assertEquals(10, VmConfig.fromConfig(tooHigh).getEstimateEnergyMaxRetry()); + + Config tooLow = withRef("vm { estimateEnergyMaxRetry = -5 }"); + assertEquals(0, VmConfig.fromConfig(tooLow).getEstimateEnergyMaxRetry()); + } + + @Test + public void testPartialConfig() { + Config config = withRef("vm { saveInternalTx = true }"); + VmConfig vm = VmConfig.fromConfig(config); + assertTrue(vm.isSaveInternalTx()); + assertFalse(vm.isSupportConstant()); // default + assertEquals(500, vm.getLruCacheSize()); // default + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // Pin every clamp in VmConfig.postProcess() so future refactors cannot + // drop them undetected (regression seen in PR #6615 with CommitteeConfig). + // =========================================================================== + + // ----- estimateEnergyMaxRetry: clamped to [0, 10] ----- + + @Test + public void testEstimateEnergyMaxRetryBoundaryValues() { + assertEquals(0, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 0 }")).getEstimateEnergyMaxRetry()); + assertEquals(10, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 10 }")).getEstimateEnergyMaxRetry()); + assertEquals(3, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 3 }")).getEstimateEnergyMaxRetry()); + } +} 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 83d7fd2c63d..f91c6a437ac 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 @@ -121,6 +121,29 @@ public class Args extends CommonParameter { @Getter private static String configFilePath = ""; + // Singleton config beans — populated at startup, read-only after init. + // New code can read directly from these beans instead of CommonParameter. + @Getter + private static NodeConfig nodeConfig; + @Getter + private static VmConfig vmConfig; + @Getter + private static BlockConfig blockConfig; + @Getter + private static CommitteeConfig committeeConfig; + @Getter + private static StorageConfig storageConfig; + @Getter + private static GenesisConfig genesisConfig; + @Getter + private static MiscConfig miscConfig; + @Getter + private static RateLimiterConfig rateLimiterConfig; + @Getter + private static MetricsConfig metricsConfig; + @Getter + private static EventConfig eventConfig; + @Getter @Setter private static LocalWitnesses localWitnesses = new LocalWitnesses(); @@ -173,873 +196,607 @@ public static void setParam(final String[] args, final String confFileName) { } /** - * Apply platform-specific constraints after all config sources are resolved. - * ARM64 does not support LevelDB (native JNI library unavailable), - * so db.engine is forced to RocksDB regardless of config or CLI settings. + * Bridge VmConfig bean values to CommonParameter fields. + * Temporary until Phase 2 moves fields into domain config objects. */ - private static void applyPlatformConstraints() { - if (Arch.isArm64() - && !Constant.ROCKSDB.equalsIgnoreCase(PARAMETER.storage.getDbEngine())) { - logger.warn("ARM64 only supports RocksDB, ignoring db.engine='{}'", - PARAMETER.storage.getDbEngine()); - PARAMETER.storage.setDbEngine(Constant.ROCKSDB); - } + private static void applyVmConfig(VmConfig vm) { + PARAMETER.supportConstant = vm.isSupportConstant(); + PARAMETER.maxEnergyLimitForConstant = vm.getMaxEnergyLimitForConstant(); + PARAMETER.lruCacheSize = vm.getLruCacheSize(); + PARAMETER.minTimeRatio = vm.getMinTimeRatio(); + PARAMETER.maxTimeRatio = vm.getMaxTimeRatio(); + PARAMETER.longRunningTime = vm.getLongRunningTime(); + PARAMETER.estimateEnergy = vm.isEstimateEnergy(); + PARAMETER.estimateEnergyMaxRetry = vm.getEstimateEnergyMaxRetry(); + PARAMETER.vmTrace = vm.isVmTrace(); + PARAMETER.saveInternalTx = vm.isSaveInternalTx(); + PARAMETER.saveFeaturedInternalTx = vm.isSaveFeaturedInternalTx(); + PARAMETER.saveCancelAllUnfreezeV2Details = vm.isSaveCancelAllUnfreezeV2Details(); } + // Old applyStorageConfig removed — merged into applyStorageConfig() + /** - * Apply parameters from config file. + * Bridge StorageConfig bean to PARAMETER.storage fields. + * Reads all storage config from one StorageConfig bean instance. + * Config param is still needed for setDefaultDbOptions/setCacheStrategies/setDbRoots + * which use raw Config for dynamic nested objects. */ - public static void applyConfigParams( - final Config config) { - - Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); - Wallet.setAddressPreFixString(Constant.ADD_PRE_FIX_STRING_MAINNET); - - PARAMETER.cryptoEngine = config.hasPath(ConfigKey.CRYPTO_ENGINE) ? config - .getString(ConfigKey.CRYPTO_ENGINE) : Constant.ECKey_ENGINE; - - if (config.hasPath(ConfigKey.VM_SUPPORT_CONSTANT)) { - PARAMETER.supportConstant = config.getBoolean(ConfigKey.VM_SUPPORT_CONSTANT); - } - - if (config.hasPath(ConfigKey.VM_MAX_ENERGY_LIMIT_FOR_CONSTANT)) { - long configLimit = config.getLong(ConfigKey.VM_MAX_ENERGY_LIMIT_FOR_CONSTANT); - PARAMETER.maxEnergyLimitForConstant = max(ENERGY_LIMIT_IN_CONSTANT_TX, configLimit, true); - } - - if (config.hasPath(ConfigKey.VM_LRU_CACHE_SIZE)) { - PARAMETER.lruCacheSize = config.getInt(ConfigKey.VM_LRU_CACHE_SIZE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_ENABLE)) { - PARAMETER.rpcEnable = config.getBoolean(ConfigKey.NODE_RPC_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_SOLIDITY_ENABLE)) { - PARAMETER.rpcSolidityEnable = config.getBoolean(ConfigKey.NODE_RPC_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_PBFT_ENABLE)) { - PARAMETER.rpcPBFTEnable = config.getBoolean(ConfigKey.NODE_RPC_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_FULLNODE_ENABLE)) { - PARAMETER.fullNodeHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_FULLNODE_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_SOLIDITY_ENABLE)) { - PARAMETER.solidityNodeHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_PBFT_ENABLE)) { - PARAMETER.pBFTHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_ENABLE)) { - PARAMETER.jsonRpcHttpFullNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_ENABLE)) { - PARAMETER.jsonRpcHttpSolidityNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_PBFT_ENABLE)) { - PARAMETER.jsonRpcHttpPBFTNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_BLOCK_RANGE)) { - PARAMETER.jsonRpcMaxBlockRange = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_BLOCK_RANGE); - } + private static void applyStorageConfig(StorageConfig sc) { + PARAMETER.storage.setDbEngine(sc.getDb().getEngine()); + PARAMETER.storage.setDbSync(sc.getDb().isSync()); + PARAMETER.storage.setDbDirectory(sc.getDb().getDirectory()); + PARAMETER.storage.setIndexDirectory(sc.getIndex().getDirectory()); + String indexSwitch = sc.getIndex().getSwitch(); + PARAMETER.storage.setIndexSwitch( + org.apache.commons.lang3.StringUtils.isNotEmpty(indexSwitch) ? indexSwitch : "on"); + PARAMETER.storage.setTransactionHistorySwitch(sc.getTransHistory().getSwitch()); + // contractParse is set in applyEventConfig — it belongs to event.subscribe domain + PARAMETER.storage.setCheckpointVersion(sc.getCheckpoint().getVersion()); + PARAMETER.storage.setCheckpointSync(sc.getCheckpoint().isSync()); + + // estimatedTransactions / maxFlushCount clamping & validation run inside + // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. + PARAMETER.storage.setEstimatedBlockTransactions(sc.getTxCache().getEstimatedTransactions()); + PARAMETER.storage.setTxCacheInitOptimization(sc.getTxCache().isInitOptimization()); + PARAMETER.storage.setMaxFlushCount(sc.getSnapshot().getMaxFlushCount()); + + // backup + StorageConfig.BackupConfig backup = sc.getBackup(); + PARAMETER.dbBackupConfig = DbBackupConfig.getInstance() + .initArgs(backup.isEnable(), backup.getPropPath(), + backup.getBak1path(), backup.getBak2path(), backup.getFrequency()); - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_SUB_TOPICS)) { - PARAMETER.jsonRpcMaxSubTopics = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_SUB_TOPICS); - } + // RocksDB settings + StorageConfig.DbSettingsConfig dbs = sc.getDbSettings(); + PARAMETER.rocksDBCustomSettings = RocksDbSettings + .initCustomSettings(dbs.getLevelNumber(), dbs.getCompactThreads(), + dbs.getBlocksize(), dbs.getMaxBytesForLevelBase(), + dbs.getMaxBytesForLevelMultiplier(), dbs.getLevel0FileNumCompactionTrigger(), + dbs.getTargetFileSizeBase(), dbs.getTargetFileSizeMultiplier(), + dbs.getMaxOpenFiles()); + RocksDbSettings.loggingSettings(); - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_BLOCK_FILTER_NUM)) { - PARAMETER.jsonRpcMaxBlockFilterNum = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_BLOCK_FILTER_NUM); - } + // Dynamic nested objects use StorageConfig's raw storage sub-tree + // setDefaultDbOptions must be called before setPropertyMapFromBean because + // createPropertyFromBean calls newDefaultDbOptions which needs defaultDbOptions initialized + PARAMETER.storage.setDefaultDbOptions(sc); + PARAMETER.storage.setPropertyMapFromBean(sc.getProperties()); + PARAMETER.storage.setCacheStrategies(sc.getRawStorageConfig()); + PARAMETER.storage.setDbRoots(sc.getRawStorageConfig()); + } - if (config.hasPath(ConfigKey.VM_MIN_TIME_RATIO)) { - PARAMETER.minTimeRatio = config.getDouble(ConfigKey.VM_MIN_TIME_RATIO); - } + /** + * Bridge NodeConfig backup sub-bean to PARAMETER fields. + */ + private static void applyNodeBackupConfig(NodeConfig nc) { + NodeConfig.NodeBackupConfig b = nc.getBackup(); + PARAMETER.backupPriority = b.getPriority(); + PARAMETER.backupPort = b.getPort(); + PARAMETER.keepAliveInterval = b.getKeepAliveInterval(); + PARAMETER.backupMembers = b.getMembers(); + } - if (config.hasPath(ConfigKey.VM_MAX_TIME_RATIO)) { - PARAMETER.maxTimeRatio = config.getDouble(ConfigKey.VM_MAX_TIME_RATIO); + /** + * Bridge GenesisConfig bean to GenesisBlock business object. + * Converts raw address strings via Base58Check decoding. + */ + private static void applyGenesisConfig(GenesisConfig gc, Config config) { + if (gc.getTimestamp().isEmpty() && gc.getAssets().isEmpty()) { + PARAMETER.genesisBlock = GenesisBlock.getDefault(); + return; } - - if (config.hasPath(ConfigKey.VM_LONG_RUNNING_TIME)) { - PARAMETER.longRunningTime = config.getInt(ConfigKey.VM_LONG_RUNNING_TIME); + PARAMETER.genesisBlock = new GenesisBlock(); + PARAMETER.genesisBlock.setTimestamp(gc.getTimestamp()); + PARAMETER.genesisBlock.setParentHash(gc.getParentHash()); + + if (!gc.getAssets().isEmpty()) { + List accounts = new ArrayList<>(); + for (GenesisConfig.AssetConfig ac : gc.getAssets()) { + Account account = new Account(); + account.setAccountName(ac.getAccountName()); + account.setAccountType(ac.getAccountType()); + account.setAddress(Commons.decodeFromBase58Check(ac.getAddress())); + account.setBalance(ac.getBalance()); + accounts.add(account); + } + PARAMETER.genesisBlock.setAssets(accounts); + AccountStore.setAccount(config); + } + { + List witnesses = new ArrayList<>(); + for (GenesisConfig.WitnessConfig wc : gc.getWitnesses()) { + Witness witness = new Witness(); + witness.setAddress(Commons.decodeFromBase58Check(wc.getAddress())); + witness.setUrl(wc.getUrl()); + witness.setVoteCount(wc.getVoteCount()); + witnesses.add(witness); + } + PARAMETER.genesisBlock.setWitnesses(witnesses); } + } - PARAMETER.storage = new Storage(); - - PARAMETER.storage.setDbEngine(Storage.getDbEngineFromConfig(config)); - PARAMETER.storage.setDbSync(Storage.getDbVersionSyncFromConfig(config)); - PARAMETER.storage.setContractParseSwitch(Storage.getContractParseSwitchFromConfig(config)); - PARAMETER.storage.setDbDirectory(Storage.getDbDirectoryFromConfig(config)); - PARAMETER.storage.setIndexDirectory(Storage.getIndexDirectoryFromConfig(config)); - PARAMETER.storage.setIndexSwitch(Storage.getIndexSwitchFromConfig(config)); - PARAMETER.storage.setTransactionHistorySwitch( - Storage.getTransactionHistorySwitchFromConfig(config)); - - PARAMETER.storage - .setCheckpointVersion(Storage.getCheckpointVersionFromConfig(config)); - PARAMETER.storage - .setCheckpointSync(Storage.getCheckpointSyncFromConfig(config)); - - PARAMETER.storage.setEstimatedBlockTransactions( - Storage.getEstimatedTransactionsFromConfig(config)); - PARAMETER.storage.setTxCacheInitOptimization( - Storage.getTxCacheInitOptimizationFromConfig(config)); - PARAMETER.storage.setMaxFlushCount(Storage.getSnapshotMaxFlushCountFromConfig(config)); - - PARAMETER.storage.setDefaultDbOptions(config); - PARAMETER.storage.setPropertyMapFromConfig(config); - PARAMETER.storage.setCacheStrategies(config); - PARAMETER.storage.setDbRoots(config); - + /** + * Bridge MiscConfig bean values to CommonParameter fields. + */ + private static void applyMiscConfig(MiscConfig mc) { + PARAMETER.cryptoEngine = mc.getCryptoEngine(); + PARAMETER.needToUpdateAsset = mc.isNeedToUpdateAsset(); + PARAMETER.historyBalanceLookup = mc.isHistoryBalanceLookup(); + PARAMETER.trxReferenceBlock = mc.getTrxReferenceBlock(); + PARAMETER.trxExpirationTimeInMilliseconds = mc.getTrxExpirationTimeInMilliseconds(); + PARAMETER.blockNumForEnergyLimit = mc.getBlockNumForEnergyLimit(); + PARAMETER.actuatorSet = mc.getActuatorWhitelist(); + + // seed.node — top-level config section, not under "node" + // Config structure is arguably misplaced but preserved for backward compatibility PARAMETER.seedNode = new SeedNode(); PARAMETER.seedNode.setAddressList( - getInetSocketAddress(config, ConfigKey.SEED_NODE_IP_LIST, false)); - - if (config.hasPath(ConfigKey.GENESIS_BLOCK)) { - PARAMETER.genesisBlock = new GenesisBlock(); + mc.getSeedNodeIpList().stream() + .map(s -> org.tron.p2p.utils.NetUtil.parseInetSocketAddress(s)) + .collect(Collectors.toList())); + } - PARAMETER.genesisBlock.setTimestamp(config.getString(ConfigKey.GENESIS_BLOCK_TIMESTAMP)); - PARAMETER.genesisBlock.setParentHash(config.getString(ConfigKey.GENESIS_BLOCK_PARENTHASH)); + /** + * Bridge RateLimiterConfig bean values to CommonParameter fields. + * HTTP/RPC rate limiter lists still use getRateLimiterFromConfig() for + * conversion to RateLimiterInitialization business objects. + */ + private static void applyRateLimiterConfig(RateLimiterConfig rl) { + PARAMETER.rateLimiterGlobalQps = rl.getGlobal().getQps(); + PARAMETER.rateLimiterGlobalIpQps = rl.getGlobal().getIp().getQps(); + PARAMETER.rateLimiterGlobalApiQps = rl.getGlobal().getApi().getQps(); + PARAMETER.rateLimiterSyncBlockChain = rl.getP2p().getSyncBlockChain(); + PARAMETER.rateLimiterFetchInvData = rl.getP2p().getFetchInvData(); + PARAMETER.rateLimiterDisconnect = rl.getP2p().getDisconnect(); + + // HTTP/RPC rate limiter items: convert bean lists to business objects + RateLimiterInitialization initialization = new RateLimiterInitialization(); + ArrayList httpItems = new ArrayList<>(); + for (RateLimiterConfig.HttpRateLimitItem item : rl.getHttp()) { + httpItems.add(new RateLimiterInitialization.HttpRateLimiterItem( + item.getComponent(), item.getStrategy(), item.getParamString())); + } + initialization.setHttpMap(httpItems); + ArrayList rpcItems = new ArrayList<>(); + for (RateLimiterConfig.RpcRateLimitItem item : rl.getRpc()) { + rpcItems.add(new RateLimiterInitialization.RpcRateLimiterItem( + item.getComponent(), item.getStrategy(), item.getParamString())); + } + initialization.setRpcMap(rpcItems); + PARAMETER.rateLimiterInitialization = initialization; + } - if (config.hasPath(ConfigKey.GENESIS_BLOCK_ASSETS)) { - PARAMETER.genesisBlock.setAssets(getAccountsFromConfig(config)); - AccountStore.setAccount(config); - } - if (config.hasPath(ConfigKey.GENESIS_BLOCK_WITNESSES)) { - PARAMETER.genesisBlock.setWitnesses(getWitnessesFromConfig(config)); + /** + * Bridge EventConfig bean values to CommonParameter fields. + * Converts EventConfig (raw bean) into EventPluginConfig and FilterQuery (business objects). + */ + private static void applyEventConfig(EventConfig ec) { + PARAMETER.eventSubscribe = ec.isEnable(); + // contractParse belongs to event.subscribe but Storage object holds it + PARAMETER.storage.setContractParseSwitch(ec.isContractParse()); + + // Build EventPluginConfig from EventConfig bean + // If event.subscribe was configured, bean will have non-default values + if (ec.isEnable() || ec.getVersion() != 0 || !ec.getTopics().isEmpty() + || StringUtils.isNotEmpty(ec.getPath()) || StringUtils.isNotEmpty(ec.getServer())) { + EventPluginConfig epc = new EventPluginConfig(); + epc.setVersion(ec.getVersion()); + epc.setStartSyncBlockNum(ec.getStartSyncBlockNum()); + + // native queue + EventConfig.NativeConfig nq = ec.getNativeQueue(); + epc.setUseNativeQueue(nq.isUseNativeQueue()); + epc.setBindPort(nq.getBindport()); + epc.setSendQueueLength(nq.getSendqueuelength()); + + if (!nq.isUseNativeQueue()) { + if (StringUtils.isNotEmpty(ec.getPath())) { + epc.setPluginPath(ec.getPath().trim()); + } + if (StringUtils.isNotEmpty(ec.getServer())) { + epc.setServerAddress(ec.getServer().trim()); + } + if (StringUtils.isNotEmpty(ec.getDbconfig())) { + epc.setDbConfig(ec.getDbconfig().trim()); + } } - } else { - PARAMETER.genesisBlock = GenesisBlock.getDefault(); - } - - PARAMETER.needSyncCheck = - config.hasPath(ConfigKey.BLOCK_NEED_SYNC_CHECK) - && config.getBoolean(ConfigKey.BLOCK_NEED_SYNC_CHECK); - - PARAMETER.nodeDiscoveryEnable = - config.hasPath(ConfigKey.NODE_DISCOVERY_ENABLE) - && config.getBoolean(ConfigKey.NODE_DISCOVERY_ENABLE); - - PARAMETER.nodeDiscoveryPersist = - config.hasPath(ConfigKey.NODE_DISCOVERY_PERSIST) - && config.getBoolean(ConfigKey.NODE_DISCOVERY_PERSIST); - - PARAMETER.nodeEffectiveCheckEnable = - config.hasPath(ConfigKey.NODE_EFFECTIVE_CHECK_ENABLE) - && config.getBoolean(ConfigKey.NODE_EFFECTIVE_CHECK_ENABLE); - - PARAMETER.nodeConnectionTimeout = - config.hasPath(ConfigKey.NODE_CONNECTION_TIMEOUT) - ? config.getInt(ConfigKey.NODE_CONNECTION_TIMEOUT) * 1000 - : 2000; - - if (!config.hasPath(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT)) { - PARAMETER.fetchBlockTimeout = 500; - } else if (config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT) > 1000) { - PARAMETER.fetchBlockTimeout = 1000; - } else if (config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT) < 100) { - PARAMETER.fetchBlockTimeout = 100; - } else { - PARAMETER.fetchBlockTimeout = config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT); - } - - PARAMETER.nodeChannelReadTimeout = - config.hasPath(ConfigKey.NODE_CHANNEL_READ_TIMEOUT) - ? config.getInt(ConfigKey.NODE_CHANNEL_READ_TIMEOUT) - : 0; - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES)) { - PARAMETER.maxConnections = config.getInt(ConfigKey.NODE_MAX_ACTIVE_NODES); - } else { - PARAMETER.maxConnections = - config.hasPath(ConfigKey.NODE_MAX_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MAX_CONNECTIONS) : 30; - } - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES) - && config.hasPath(ConfigKey.NODE_CONNECT_FACTOR)) { - PARAMETER.minConnections = (int) (PARAMETER.maxConnections - * config.getDouble(ConfigKey.NODE_CONNECT_FACTOR)); - } else { - PARAMETER.minConnections = - config.hasPath(ConfigKey.NODE_MIN_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MIN_CONNECTIONS) : 8; - } - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES) - && config.hasPath(ConfigKey.NODE_ACTIVE_CONNECT_FACTOR)) { - PARAMETER.minActiveConnections = (int) (PARAMETER.maxConnections - * config.getDouble(ConfigKey.NODE_ACTIVE_CONNECT_FACTOR)); - } else { - PARAMETER.minActiveConnections = - config.hasPath(ConfigKey.NODE_MIN_ACTIVE_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MIN_ACTIVE_CONNECTIONS) : 3; - } - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES_WITH_SAME_IP)) { - PARAMETER.maxConnectionsWithSameIp = - config.getInt(ConfigKey.NODE_MAX_ACTIVE_NODES_WITH_SAME_IP); - } else { - PARAMETER.maxConnectionsWithSameIp = - config.hasPath(ConfigKey.NODE_MAX_CONNECTIONS_WITH_SAME_IP) ? config - .getInt(ConfigKey.NODE_MAX_CONNECTIONS_WITH_SAME_IP) : 2; - } - - PARAMETER.maxTps = config.hasPath(ConfigKey.NODE_MAX_TPS) - ? config.getInt(ConfigKey.NODE_MAX_TPS) : 1000; - - PARAMETER.minParticipationRate = - config.hasPath(ConfigKey.NODE_MIN_PARTICIPATION_RATE) - ? config.getInt(ConfigKey.NODE_MIN_PARTICIPATION_RATE) - : 0; - - PARAMETER.p2pConfig = new P2pConfig(); - PARAMETER.nodeListenPort = - config.hasPath(ConfigKey.NODE_LISTEN_PORT) - ? config.getInt(ConfigKey.NODE_LISTEN_PORT) : 0; - - PARAMETER.nodeLanIp = PARAMETER.p2pConfig.getLanIp(); - externalIp(config); - - PARAMETER.nodeP2pVersion = - config.hasPath(ConfigKey.NODE_P2P_VERSION) - ? config.getInt(ConfigKey.NODE_P2P_VERSION) : 0; - - PARAMETER.nodeEnableIpv6 = - config.hasPath(ConfigKey.NODE_ENABLE_IPV6) && config.getBoolean(ConfigKey.NODE_ENABLE_IPV6); - - PARAMETER.dnsTreeUrls = config.hasPath(ConfigKey.NODE_DNS_TREE_URLS) ? config.getStringList( - ConfigKey.NODE_DNS_TREE_URLS) : new ArrayList<>(); - - PARAMETER.dnsPublishConfig = loadDnsPublishConfig(config); + // topics + List triggerConfigs = new ArrayList<>(); + for (EventConfig.TopicConfig tc : ec.getTopics()) { + TriggerConfig trig = new TriggerConfig(); + trig.setTriggerName(tc.getTriggerName()); + trig.setEnabled(tc.isEnable()); + trig.setTopic(tc.getTopic()); + trig.setSolidified(tc.isSolidified()); + trig.setEthCompatible(tc.isEthCompatible()); + trig.setRedundancy(tc.isRedundancy()); + triggerConfigs.add(trig); + } + epc.setTriggerConfigList(triggerConfigs); - PARAMETER.syncFetchBatchNum = config.hasPath(ConfigKey.NODE_SYNC_FETCH_BATCH_NUM) ? config - .getInt(ConfigKey.NODE_SYNC_FETCH_BATCH_NUM) : 2000; - if (PARAMETER.syncFetchBatchNum > 2000) { - PARAMETER.syncFetchBatchNum = 2000; - } - if (PARAMETER.syncFetchBatchNum < 100) { - PARAMETER.syncFetchBatchNum = 100; + PARAMETER.eventPluginConfig = epc; } - PARAMETER.rpcPort = - config.hasPath(ConfigKey.NODE_RPC_PORT) - ? config.getInt(ConfigKey.NODE_RPC_PORT) : 50051; - - PARAMETER.rpcOnSolidityPort = - config.hasPath(ConfigKey.NODE_RPC_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_RPC_SOLIDITY_PORT) : 50061; - - PARAMETER.rpcOnPBFTPort = - config.hasPath(ConfigKey.NODE_RPC_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_RPC_PBFT_PORT) : 50071; - - PARAMETER.fullNodeHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_FULLNODE_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_FULLNODE_PORT) : 8090; - - PARAMETER.solidityHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_SOLIDITY_PORT) : 8091; - - PARAMETER.pBFTHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_PBFT_PORT) : 8092; - - PARAMETER.jsonRpcHttpFullNodePort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_PORT) : 8545; - - PARAMETER.jsonRpcHttpSolidityPort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_PORT) : 8555; - - PARAMETER.jsonRpcHttpPBFTPort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_PBFT_PORT) : 8565; - - PARAMETER.rpcThreadNum = - config.hasPath(ConfigKey.NODE_RPC_THREAD) ? config.getInt(ConfigKey.NODE_RPC_THREAD) - : (Runtime.getRuntime().availableProcessors() + 1) / 2; - - PARAMETER.solidityThreads = - config.hasPath(ConfigKey.NODE_SOLIDITY_THREADS) - ? config.getInt(ConfigKey.NODE_SOLIDITY_THREADS) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.maxConcurrentCallsPerConnection = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION) - ? config.getInt(ConfigKey.NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION) - : Integer.MAX_VALUE; - - PARAMETER.flowControlWindow = config.hasPath(ConfigKey.NODE_RPC_FLOW_CONTROL_WINDOW) - ? config.getInt(ConfigKey.NODE_RPC_FLOW_CONTROL_WINDOW) - : NettyServerBuilder.DEFAULT_FLOW_CONTROL_WINDOW; - if (config.hasPath(ConfigKey.NODE_RPC_MAX_RST_STREAM)) { - PARAMETER.rpcMaxRstStream = config.getInt(ConfigKey.NODE_RPC_MAX_RST_STREAM); - } - if (config.hasPath(ConfigKey.NODE_RPC_SECONDS_PER_WINDOW)) { - PARAMETER.rpcSecondsPerWindow = config.getInt(ConfigKey.NODE_RPC_SECONDS_PER_WINDOW); - } + // Build FilterQuery from EventConfig.FilterConfig bean + EventConfig.FilterConfig fc = ec.getFilter(); + if (StringUtils.isNotEmpty(fc.getFromblock()) || StringUtils.isNotEmpty(fc.getToblock()) + || !fc.getContractAddress().isEmpty()) { + FilterQuery filter = new FilterQuery(); - PARAMETER.maxConnectionIdleInMillis = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS) - ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS) - : Long.MAX_VALUE; + try { + filter.setFromBlock(FilterQuery.parseFromBlockNumber(fc.getFromblock().trim())); + } catch (Exception e) { + logger.error("invalid filter: fromBlockNumber: {}", fc.getFromblock(), e); + PARAMETER.eventFilter = null; + return; + } - PARAMETER.blockProducedTimeOut = config.hasPath(ConfigKey.NODE_PRODUCED_TIMEOUT) - ? config.getInt(ConfigKey.NODE_PRODUCED_TIMEOUT) : BLOCK_PRODUCE_TIMEOUT_PERCENT; + try { + filter.setToBlock(FilterQuery.parseToBlockNumber(fc.getToblock().trim())); + } catch (Exception e) { + logger.error("invalid filter: toBlockNumber: {}", fc.getToblock(), e); + PARAMETER.eventFilter = null; + return; + } - PARAMETER.maxHttpConnectNumber = config.hasPath(ConfigKey.NODE_MAX_HTTP_CONNECT_NUMBER) - ? config.getInt(ConfigKey.NODE_MAX_HTTP_CONNECT_NUMBER) - : NodeConstant.MAX_HTTP_CONNECT_NUMBER; + filter.setContractAddressList( + fc.getContractAddress().stream() + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList())); + filter.setContractTopicList( + fc.getContractTopic().stream() + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList())); - if (PARAMETER.blockProducedTimeOut < 30) { - PARAMETER.blockProducedTimeOut = 30; - } - if (PARAMETER.blockProducedTimeOut > 100) { - PARAMETER.blockProducedTimeOut = 100; + PARAMETER.eventFilter = filter; } + } - PARAMETER.netMaxTrxPerSecond = config.hasPath(ConfigKey.NODE_NET_MAX_TRX_PER_SECOND) - ? config.getInt(ConfigKey.NODE_NET_MAX_TRX_PER_SECOND) - : NetConstants.NET_MAX_TRX_PER_SECOND; - - PARAMETER.maxConnectionAgeInMillis = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) - ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) - : Long.MAX_VALUE; - - PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - - PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) - : GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; - - PARAMETER.isRpcReflectionServiceEnable = - config.hasPath(ConfigKey.NODE_RPC_REFLECTION_SERVICE) - && config.getBoolean(ConfigKey.NODE_RPC_REFLECTION_SERVICE); - - PARAMETER.maintenanceTimeInterval = - config.hasPath(ConfigKey.BLOCK_MAINTENANCE_TIME_INTERVAL) ? config - .getInt(ConfigKey.BLOCK_MAINTENANCE_TIME_INTERVAL) : 21600000L; - - PARAMETER.proposalExpireTime = getProposalExpirationTime(config); - - PARAMETER.checkFrozenTime = - config.hasPath(ConfigKey.BLOCK_CHECK_FROZEN_TIME) ? config - .getInt(ConfigKey.BLOCK_CHECK_FROZEN_TIME) : 1; - - PARAMETER.allowCreationOfContracts = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_CREATION_OF_CONTRACTS) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_CREATION_OF_CONTRACTS) : 0; - - PARAMETER.allowMultiSign = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_MULTI_SIGN) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_MULTI_SIGN) : 0; - - PARAMETER.allowAdaptiveEnergy = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ADAPTIVE_ENERGY) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ADAPTIVE_ENERGY) : 0; - - PARAMETER.allowDelegateResource = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_DELEGATE_RESOURCE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_DELEGATE_RESOURCE) : 0; - - PARAMETER.allowSameTokenName = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_SAME_TOKEN_NAME) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_SAME_TOKEN_NAME) : 0; - - PARAMETER.allowTvmTransferTrc10 = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_TRANSFER_TRC10) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_TRANSFER_TRC10) : 0; - - PARAMETER.allowTvmConstantinople = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_CONSTANTINOPLE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_CONSTANTINOPLE) : 0; - - PARAMETER.allowTvmSolidity059 = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_SOLIDITY059) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_SOLIDITY059) : 0; + /** + * Bridge MetricsConfig bean values to CommonParameter fields. + * Note: node.metricsEnable is handled in applyNodeConfig (it's a node-level field). + */ + private static void applyMetricsConfig(MetricsConfig mc) { + PARAMETER.metricsStorageEnable = mc.isStorageEnable(); + PARAMETER.influxDbIp = mc.getInfluxdb().getIp().isEmpty() + ? Constant.LOCAL_HOST : mc.getInfluxdb().getIp(); + PARAMETER.influxDbPort = mc.getInfluxdb().getPort(); + PARAMETER.influxDbDatabase = mc.getInfluxdb().getDatabase(); + PARAMETER.metricsReportInterval = mc.getInfluxdb().getMetricsReportInterval(); + PARAMETER.metricsPrometheusEnable = mc.getPrometheus().isEnable(); + PARAMETER.metricsPrometheusPort = mc.getPrometheus().getPort(); + } - PARAMETER.forbidTransferToContract = - config.hasPath(ConfigKey.COMMITTEE_FORBID_TRANSFER_TO_CONTRACT) ? config - .getInt(ConfigKey.COMMITTEE_FORBID_TRANSFER_TO_CONTRACT) : 0; + /** + * Bridge CommitteeConfig bean values to CommonParameter fields. + */ + private static void applyCommitteeConfig(CommitteeConfig cc) { + PARAMETER.allowCreationOfContracts = cc.getAllowCreationOfContracts(); + PARAMETER.allowMultiSign = (int) cc.getAllowMultiSign(); + PARAMETER.allowAdaptiveEnergy = cc.getAllowAdaptiveEnergy(); + PARAMETER.allowDelegateResource = cc.getAllowDelegateResource(); + PARAMETER.allowSameTokenName = cc.getAllowSameTokenName(); + PARAMETER.allowTvmTransferTrc10 = cc.getAllowTvmTransferTrc10(); + PARAMETER.allowTvmConstantinople = cc.getAllowTvmConstantinople(); + PARAMETER.allowTvmSolidity059 = cc.getAllowTvmSolidity059(); + PARAMETER.forbidTransferToContract = cc.getForbidTransferToContract(); + PARAMETER.allowShieldedTRC20Transaction = cc.getAllowShieldedTRC20Transaction(); + PARAMETER.allowMarketTransaction = cc.getAllowMarketTransaction(); + PARAMETER.allowTransactionFeePool = cc.getAllowTransactionFeePool(); + PARAMETER.allowBlackHoleOptimization = cc.getAllowBlackHoleOptimization(); + PARAMETER.allowNewResourceModel = cc.getAllowNewResourceModel(); + PARAMETER.allowTvmIstanbul = cc.getAllowTvmIstanbul(); + PARAMETER.allowProtoFilterNum = cc.getAllowProtoFilterNum(); + PARAMETER.allowAccountStateRoot = cc.getAllowAccountStateRoot(); + PARAMETER.changedDelegation = cc.getChangedDelegation(); + PARAMETER.allowPBFT = cc.getAllowPBFT(); + PARAMETER.pBFTExpireNum = cc.getPBFTExpireNum(); + PARAMETER.allowTvmFreeze = cc.getAllowTvmFreeze(); + PARAMETER.allowTvmVote = cc.getAllowTvmVote(); + PARAMETER.allowTvmLondon = cc.getAllowTvmLondon(); + PARAMETER.allowTvmCompatibleEvm = cc.getAllowTvmCompatibleEvm(); + PARAMETER.allowHigherLimitForMaxCpuTimeOfOneTx = + cc.getAllowHigherLimitForMaxCpuTimeOfOneTx(); + PARAMETER.allowNewRewardAlgorithm = cc.getAllowNewRewardAlgorithm(); + PARAMETER.allowOptimizedReturnValueOfChainId = + cc.getAllowOptimizedReturnValueOfChainId(); + PARAMETER.allowTvmShangHai = cc.getAllowTvmShangHai(); + PARAMETER.allowOldRewardOpt = cc.getAllowOldRewardOpt(); + PARAMETER.allowEnergyAdjustment = cc.getAllowEnergyAdjustment(); + PARAMETER.allowStrictMath = cc.getAllowStrictMath(); + PARAMETER.consensusLogicOptimization = cc.getConsensusLogicOptimization(); + PARAMETER.allowTvmCancun = cc.getAllowTvmCancun(); + PARAMETER.allowTvmBlob = cc.getAllowTvmBlob(); + PARAMETER.allowTvmOsaka = cc.getAllowTvmOsaka(); + PARAMETER.unfreezeDelayDays = cc.getUnfreezeDelayDays(); + // allowReceiptsMerkleRoot not in CommonParameter — skip for now + PARAMETER.allowAccountAssetOptimization = cc.getAllowAccountAssetOptimization(); + PARAMETER.allowAssetOptimization = cc.getAllowAssetOptimization(); + PARAMETER.allowNewReward = cc.getAllowNewReward(); + PARAMETER.memoFee = cc.getMemoFee(); + PARAMETER.allowDelegateOptimization = cc.getAllowDelegateOptimization(); + PARAMETER.allowDynamicEnergy = cc.getAllowDynamicEnergy(); + PARAMETER.dynamicEnergyThreshold = cc.getDynamicEnergyThreshold(); + PARAMETER.dynamicEnergyIncreaseFactor = cc.getDynamicEnergyIncreaseFactor(); + PARAMETER.dynamicEnergyMaxFactor = cc.getDynamicEnergyMaxFactor(); + } - PARAMETER.tcpNettyWorkThreadNum = config.hasPath(ConfigKey.NODE_TCP_NETTY_WORK_THREAD_NUM) - ? config.getInt(ConfigKey.NODE_TCP_NETTY_WORK_THREAD_NUM) : 0; + /** + * Bridge BlockConfig bean values to CommonParameter fields. + */ + private static void applyBlockConfig(BlockConfig block) { + PARAMETER.needSyncCheck = block.isNeedSyncCheck(); + PARAMETER.maintenanceTimeInterval = block.getMaintenanceTimeInterval(); + PARAMETER.proposalExpireTime = block.getProposalExpireTime(); + PARAMETER.checkFrozenTime = block.getCheckFrozenTime(); + } - PARAMETER.udpNettyWorkThreadNum = config.hasPath(ConfigKey.NODE_UDP_NETTY_WORK_THREAD_NUM) - ? config.getInt(ConfigKey.NODE_UDP_NETTY_WORK_THREAD_NUM) : 1; + /** + * Bridge NodeConfig bean values to CommonParameter fields. + * Some fields require post-binding range checks or dynamic defaults (e.g. CPUs/2), + * which are applied here after copying the bean value. + * + * @param nc the NodeConfig bean populated from config.conf "node" section + * node.discovery / node.channel.read.timeout (dot-notation paths + * not part of the NodeConfig bean) + */ + @SuppressWarnings("checkstyle:MethodLength") + private static void applyNodeConfig(NodeConfig nc) { + // ---- RPC sub-bean ---- + NodeConfig.RpcConfig rpc = nc.getRpc(); + PARAMETER.rpcEnable = rpc.isEnable(); + PARAMETER.rpcSolidityEnable = rpc.isSolidityEnable(); + PARAMETER.rpcPBFTEnable = rpc.isPBFTEnable(); + PARAMETER.rpcPort = rpc.getPort(); + PARAMETER.rpcOnSolidityPort = rpc.getSolidityPort(); + PARAMETER.rpcOnPBFTPort = rpc.getPBFTPort(); + PARAMETER.rpcThreadNum = rpc.getThread(); + PARAMETER.maxConcurrentCallsPerConnection = rpc.getMaxConcurrentCallsPerConnection(); + PARAMETER.flowControlWindow = rpc.getFlowControlWindow(); + PARAMETER.rpcMaxRstStream = rpc.getMaxRstStream(); + PARAMETER.rpcSecondsPerWindow = rpc.getSecondsPerWindow(); + PARAMETER.maxConnectionIdleInMillis = rpc.getMaxConnectionIdleInMillis(); + PARAMETER.maxConnectionAgeInMillis = rpc.getMaxConnectionAgeInMillis(); + PARAMETER.maxMessageSize = rpc.getMaxMessageSize(); + PARAMETER.maxHeaderListSize = rpc.getMaxHeaderListSize(); + PARAMETER.isRpcReflectionServiceEnable = rpc.isReflectionService(); + PARAMETER.minEffectiveConnection = rpc.getMinEffectiveConnection(); + PARAMETER.trxCacheEnable = rpc.isTrxCacheEnable(); + + // ---- HTTP sub-bean ---- + NodeConfig.HttpConfig http = nc.getHttp(); + PARAMETER.fullNodeHttpEnable = http.isFullNodeEnable(); + PARAMETER.solidityNodeHttpEnable = http.isSolidityEnable(); + PARAMETER.pBFTHttpEnable = http.isPBFTEnable(); + PARAMETER.fullNodeHttpPort = http.getFullNodePort(); + PARAMETER.solidityHttpPort = http.getSolidityPort(); + PARAMETER.pBFTHttpPort = http.getPBFTPort(); + + // ---- JSON-RPC sub-bean ---- + NodeConfig.JsonRpcConfig jsonrpc = nc.getJsonrpc(); + PARAMETER.jsonRpcHttpFullNodeEnable = jsonrpc.isHttpFullNodeEnable(); + PARAMETER.jsonRpcHttpSolidityNodeEnable = jsonrpc.isHttpSolidityEnable(); + PARAMETER.jsonRpcHttpPBFTNodeEnable = jsonrpc.isHttpPBFTEnable(); + PARAMETER.jsonRpcHttpFullNodePort = jsonrpc.getHttpFullNodePort(); + PARAMETER.jsonRpcHttpSolidityPort = jsonrpc.getHttpSolidityPort(); + PARAMETER.jsonRpcHttpPBFTPort = jsonrpc.getHttpPBFTPort(); + PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange(); + PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics(); + PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); + + // ---- P2P sub-bean ---- + PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); + + // ---- DNS sub-bean (tree URLs only — publish config uses complex validation) ---- + PARAMETER.dnsTreeUrls = nc.getDns().getTreeUrls().isEmpty() + ? new ArrayList<>() : new ArrayList<>(nc.getDns().getTreeUrls()); + + // ---- Dynamic config sub-bean ---- + PARAMETER.dynamicConfigEnable = nc.getDynamicConfig().isEnable(); + PARAMETER.dynamicConfigCheckInterval = nc.getDynamicConfig().getCheckInterval(); + + // ---- Flat scalar fields ---- + PARAMETER.nodeEffectiveCheckEnable = nc.isEffectiveCheckEnable(); + PARAMETER.nodeConnectionTimeout = nc.getConnectionTimeout() * 1000; + + // fetchBlock.timeout — range check [100, 1000], default 500 + int fetchTimeout = nc.getFetchBlockTimeout(); + if (fetchTimeout > 1000) { + fetchTimeout = 1000; + } else if (fetchTimeout < 100) { + fetchTimeout = 100; + } + PARAMETER.fetchBlockTimeout = fetchTimeout; + + PARAMETER.maxConnections = nc.getMaxConnections(); + PARAMETER.minConnections = nc.getMinConnections(); + PARAMETER.minActiveConnections = nc.getMinActiveConnections(); + PARAMETER.maxConnectionsWithSameIp = nc.getMaxConnectionsWithSameIp(); + PARAMETER.maxTps = nc.getMaxTps(); + PARAMETER.minParticipationRate = nc.getMinParticipationRate(); + PARAMETER.nodeListenPort = nc.getListenPort(); + PARAMETER.nodeEnableIpv6 = nc.isEnableIpv6(); + + PARAMETER.syncFetchBatchNum = nc.getSyncFetchBatchNum(); + PARAMETER.solidityThreads = nc.getSolidityThreads(); + PARAMETER.blockProducedTimeOut = nc.getBlockProducedTimeOut(); + + PARAMETER.maxHttpConnectNumber = nc.getMaxHttpConnectNumber(); + PARAMETER.netMaxTrxPerSecond = nc.getNetMaxTrxPerSecond(); + PARAMETER.tcpNettyWorkThreadNum = nc.getTcpNettyWorkThreadNum(); + PARAMETER.udpNettyWorkThreadNum = nc.getUdpNettyWorkThreadNum(); if (StringUtils.isEmpty(PARAMETER.trustNodeAddr)) { - PARAMETER.trustNodeAddr = - config.hasPath(ConfigKey.NODE_TRUST_NODE) - ? config.getString(ConfigKey.NODE_TRUST_NODE) : null; - } - - PARAMETER.validateSignThreadNum = - config.hasPath(ConfigKey.NODE_VALIDATE_SIGN_THREAD_NUM) ? config - .getInt(ConfigKey.NODE_VALIDATE_SIGN_THREAD_NUM) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.walletExtensionApi = - config.hasPath(ConfigKey.NODE_WALLET_EXTENSION_API) - && config.getBoolean(ConfigKey.NODE_WALLET_EXTENSION_API); - PARAMETER.estimateEnergy = - config.hasPath(ConfigKey.VM_ESTIMATE_ENERGY) - && config.getBoolean(ConfigKey.VM_ESTIMATE_ENERGY); - PARAMETER.estimateEnergyMaxRetry = config.hasPath(ConfigKey.VM_ESTIMATE_ENERGY_MAX_RETRY) - ? config.getInt(ConfigKey.VM_ESTIMATE_ENERGY_MAX_RETRY) : 3; - if (PARAMETER.estimateEnergyMaxRetry < 0) { - PARAMETER.estimateEnergyMaxRetry = 0; - } - if (PARAMETER.estimateEnergyMaxRetry > 10) { - PARAMETER.estimateEnergyMaxRetry = 10; + String trustNode = nc.getTrustNode(); + PARAMETER.trustNodeAddr = StringUtils.isEmpty(trustNode) ? null : trustNode; } - PARAMETER.receiveTcpMinDataLength = config.hasPath(ConfigKey.NODE_RECEIVE_TCP_MIN_DATA_LENGTH) - ? config.getLong(ConfigKey.NODE_RECEIVE_TCP_MIN_DATA_LENGTH) : 2048; + PARAMETER.validateSignThreadNum = nc.getValidateSignThreadNum(); + PARAMETER.walletExtensionApi = nc.isWalletExtensionApi(); + PARAMETER.receiveTcpMinDataLength = nc.getReceiveTcpMinDataLength(); + PARAMETER.isOpenFullTcpDisconnect = nc.isOpenFullTcpDisconnect(); + PARAMETER.nodeDetectEnable = nc.isNodeDetectEnable(); - PARAMETER.isOpenFullTcpDisconnect = config.hasPath(ConfigKey.NODE_IS_OPEN_FULL_TCP_DISCONNECT) - && config.getBoolean(ConfigKey.NODE_IS_OPEN_FULL_TCP_DISCONNECT); + PARAMETER.inactiveThreshold = nc.getInactiveThreshold(); - PARAMETER.nodeDetectEnable = config.hasPath(ConfigKey.NODE_DETECT_ENABLE) - && config.getBoolean(ConfigKey.NODE_DETECT_ENABLE); + PARAMETER.maxTransactionPendingSize = nc.getMaxTransactionPendingSize(); + PARAMETER.pendingTransactionTimeout = nc.getPendingTransactionTimeout(); - PARAMETER.inactiveThreshold = config.hasPath(ConfigKey.NODE_INACTIVE_THRESHOLD) - ? config.getInt(ConfigKey.NODE_INACTIVE_THRESHOLD) : 600; - if (PARAMETER.inactiveThreshold < 1) { - PARAMETER.inactiveThreshold = 1; - } + PARAMETER.validContractProtoThreadNum = nc.getValidContractProtoThreads(); - PARAMETER.maxTransactionPendingSize = - config.hasPath(ConfigKey.NODE_MAX_TRANSACTION_PENDING_SIZE) - ? config.getInt(ConfigKey.NODE_MAX_TRANSACTION_PENDING_SIZE) : 2000; + PARAMETER.maxFastForwardNum = nc.getMaxFastForwardNum(); + PARAMETER.shieldedTransInPendingMaxCounts = nc.getShieldedTransInPendingMaxCounts(); + PARAMETER.agreeNodeCount = nc.getAgreeNodeCount(); - PARAMETER.pendingTransactionTimeout = config.hasPath(ConfigKey.NODE_PENDING_TRANSACTION_TIMEOUT) - ? config.getLong(ConfigKey.NODE_PENDING_TRANSACTION_TIMEOUT) : 60_000; + PARAMETER.setOpenHistoryQueryWhenLiteFN(nc.isOpenHistoryQueryWhenLiteFN()); + PARAMETER.nodeMetricsEnable = nc.isMetricsEnable(); + PARAMETER.openPrintLog = nc.isOpenPrintLog(); + PARAMETER.openTransactionSort = nc.isOpenTransactionSort(); + PARAMETER.blockCacheTimeout = nc.getBlockCacheTimeout(); + PARAMETER.zenTokenId = nc.getZenTokenId(); + PARAMETER.allowShieldedTransactionApi = nc.isAllowShieldedTransactionApi(); - PARAMETER.needToUpdateAsset = - !config.hasPath(ConfigKey.STORAGE_NEEDTO_UPDATE_ASSET) || config - .getBoolean(ConfigKey.STORAGE_NEEDTO_UPDATE_ASSET); - PARAMETER.trxReferenceBlock = config.hasPath(ConfigKey.TRX_REFERENCE_BLOCK) - ? config.getString(ConfigKey.TRX_REFERENCE_BLOCK) : "solid"; + PARAMETER.unsolidifiedBlockCheck = nc.isUnsolidifiedBlockCheck(); + PARAMETER.maxUnsolidifiedBlocks = nc.getMaxUnsolidifiedBlocks(); - PARAMETER.trxExpirationTimeInMilliseconds = - config.hasPath(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) - && config.getLong(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) > 0 - ? config.getLong(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) - : Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME; + // disabledApi list — lowercase normalization + PARAMETER.disabledApiList = nc.getDisabledApi().isEmpty() + ? Collections.emptyList() + : nc.getDisabledApi().stream().map(String::toLowerCase) + .collect(Collectors.toList()); - PARAMETER.minEffectiveConnection = config.hasPath(ConfigKey.NODE_RPC_MIN_EFFECTIVE_CONNECTION) - ? config.getInt(ConfigKey.NODE_RPC_MIN_EFFECTIVE_CONNECTION) : 1; + // ---- Fields previously scattered in applyConfigParams ---- - PARAMETER.trxCacheEnable = config.hasPath(ConfigKey.NODE_RPC_TRX_CACHE_ENABLE) - && config.getBoolean(ConfigKey.NODE_RPC_TRX_CACHE_ENABLE); + // discovery (dot-notation, read in NodeConfig.fromConfig) + PARAMETER.nodeDiscoveryEnable = nc.isDiscoveryEnable(); + PARAMETER.nodeDiscoveryPersist = nc.isDiscoveryPersist(); + PARAMETER.nodeChannelReadTimeout = nc.getChannelReadTimeout(); - PARAMETER.blockNumForEnergyLimit = config.hasPath(ConfigKey.ENERGY_LIMIT_BLOCK_NUM) - ? config.getInt(ConfigKey.ENERGY_LIMIT_BLOCK_NUM) : 4727890L; + // Legacy maxActiveNodes fallback handled in NodeConfig.fromConfig() - PARAMETER.vmTrace = - config.hasPath(ConfigKey.VM_TRACE) && config.getBoolean(ConfigKey.VM_TRACE); - - PARAMETER.saveInternalTx = - config.hasPath(ConfigKey.VM_SAVE_INTERNAL_TX) - && config.getBoolean(ConfigKey.VM_SAVE_INTERNAL_TX); - - PARAMETER.saveFeaturedInternalTx = - config.hasPath(ConfigKey.VM_SAVE_FEATURED_INTERNAL_TX) - && config.getBoolean(ConfigKey.VM_SAVE_FEATURED_INTERNAL_TX); + // p2p config and external IP + PARAMETER.p2pConfig = new P2pConfig(); + PARAMETER.nodeLanIp = PARAMETER.p2pConfig.getLanIp(); + externalIp(nc); - if (!PARAMETER.saveCancelAllUnfreezeV2Details - && config.hasPath(ConfigKey.VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS)) { - PARAMETER.saveCancelAllUnfreezeV2Details = - config.getBoolean(ConfigKey.VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS); - } + // DNS publish config + PARAMETER.dnsPublishConfig = loadDnsPublishConfig(nc); - if (PARAMETER.saveCancelAllUnfreezeV2Details - && (!PARAMETER.saveInternalTx || !PARAMETER.saveFeaturedInternalTx)) { - logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as " - + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); - } + // Shielded transaction API — legacy fallback handled in NodeConfig.fromConfig() + PARAMETER.allowShieldedTransactionApi = nc.isAllowShieldedTransactionApi(); - // PARAMETER.allowShieldedTransaction = - // config.hasPath(Constant.COMMITTEE_ALLOW_SHIELDED_TRANSACTION) ? config - // .getInt(Constant.COMMITTEE_ALLOW_SHIELDED_TRANSACTION) : 0; - - PARAMETER.allowShieldedTRC20Transaction = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION) : 0; - - PARAMETER.allowMarketTransaction = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_MARKET_TRANSACTION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_MARKET_TRANSACTION) : 0; - - PARAMETER.allowTransactionFeePool = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TRANSACTION_FEE_POOL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TRANSACTION_FEE_POOL) : 0; - - PARAMETER.allowBlackHoleOptimization = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION) : 0; - - PARAMETER.allowNewResourceModel = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_NEW_RESOURCE_MODEL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_NEW_RESOURCE_MODEL) : 0; - - PARAMETER.allowTvmIstanbul = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_ISTANBUL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_ISTANBUL) : 0; - - PARAMETER.eventPluginConfig = - config.hasPath(ConfigKey.EVENT_SUBSCRIBE) - ? getEventPluginConfig(config) : null; - - PARAMETER.eventFilter = - config.hasPath(ConfigKey.EVENT_SUBSCRIBE_FILTER) ? getEventFilter(config) : null; - - PARAMETER.eventSubscribe = config.hasPath(ConfigKey.EVENT_SUBSCRIBE_ENABLE) - && config.getBoolean(ConfigKey.EVENT_SUBSCRIBE_ENABLE); - - if (config.hasPath(ConfigKey.ALLOW_SHIELDED_TRANSACTION_API)) { - PARAMETER.allowShieldedTransactionApi = - config.getBoolean(ConfigKey.ALLOW_SHIELDED_TRANSACTION_API); - } else if (config.hasPath(ConfigKey.NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION)) { - // for compatibility with previous configuration - PARAMETER.allowShieldedTransactionApi = - config.getBoolean(ConfigKey.NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION); - logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " - + "Please use [node.allowShieldedTransactionApi] instead."); - } else { - PARAMETER.allowShieldedTransactionApi = true; + // Active/passive/fastForward node lists from bean with filtering + PARAMETER.activeNodes = filterInetSocketAddress(nc.getActive(), true); + PARAMETER.passiveNodes = new ArrayList<>(); + for (InetSocketAddress sa : filterInetSocketAddress(nc.getPassive(), false)) { + PARAMETER.passiveNodes.add(sa.getAddress()); } + PARAMETER.fastForwardNodes = filterInetSocketAddress(nc.getFastForward(), true); - PARAMETER.zenTokenId = config.hasPath(ConfigKey.NODE_ZEN_TOKENID) - ? config.getString(ConfigKey.NODE_ZEN_TOKENID) : "000000"; - - PARAMETER.allowProtoFilterNum = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_PROTO_FILTER_NUM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_PROTO_FILTER_NUM) : 0; - - PARAMETER.allowAccountStateRoot = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT) : 0; - - PARAMETER.validContractProtoThreadNum = - config.hasPath(ConfigKey.NODE_VALID_CONTRACT_PROTO_THREADS) ? config - .getInt(ConfigKey.NODE_VALID_CONTRACT_PROTO_THREADS) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.activeNodes = getInetSocketAddress(config, ConfigKey.NODE_ACTIVE, true); - - PARAMETER.passiveNodes = getInetAddress(config, ConfigKey.NODE_PASSIVE); - - PARAMETER.fastForwardNodes = getInetSocketAddress(config, ConfigKey.NODE_FAST_FORWARD, true); - - PARAMETER.maxFastForwardNum = config.hasPath(ConfigKey.NODE_MAX_FAST_FORWARD_NUM) ? config - .getInt(ConfigKey.NODE_MAX_FAST_FORWARD_NUM) : 4; - if (PARAMETER.maxFastForwardNum > MAX_ACTIVE_WITNESS_NUM) { - PARAMETER.maxFastForwardNum = MAX_ACTIVE_WITNESS_NUM; - } - if (PARAMETER.maxFastForwardNum < 1) { - PARAMETER.maxFastForwardNum = 1; + // node.shutdown from bean (dot-notation, read in NodeConfig.fromConfig) + String shutdownBlockTime = nc.getShutdownBlockTime(); + if (!shutdownBlockTime.isEmpty()) { + try { + PARAMETER.shutdownBlockTime = new CronExpression(shutdownBlockTime); + } catch (ParseException e) { + throw new TronError(e, TronError.ErrCode.AUTO_STOP_PARAMS); + } } - - PARAMETER.shieldedTransInPendingMaxCounts = - config.hasPath(ConfigKey.NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS) ? config - .getInt(ConfigKey.NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS) : 10; - - PARAMETER.rateLimiterGlobalQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_QPS) : 50000; - - PARAMETER.rateLimiterGlobalIpQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_IP_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_IP_QPS) : 10000; - - PARAMETER.rateLimiterGlobalApiQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_API_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_API_QPS) : 1000; - - PARAMETER.rateLimiterInitialization = getRateLimiterFromConfig(config); - - PARAMETER.rateLimiterSyncBlockChain = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN) : 3.0; - - PARAMETER.rateLimiterFetchInvData = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_FETCH_INV_DATA) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_FETCH_INV_DATA) : 3.0; - - PARAMETER.rateLimiterDisconnect = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_DISCONNECT) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_DISCONNECT) : 1.0; - - PARAMETER.changedDelegation = - config.hasPath(ConfigKey.COMMITTEE_CHANGED_DELEGATION) ? config - .getInt(ConfigKey.COMMITTEE_CHANGED_DELEGATION) : 0; - - PARAMETER.allowPBFT = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_PBFT) ? config - .getLong(ConfigKey.COMMITTEE_ALLOW_PBFT) : 0; - - PARAMETER.pBFTExpireNum = - config.hasPath(ConfigKey.COMMITTEE_PBFT_EXPIRE_NUM) ? config - .getLong(ConfigKey.COMMITTEE_PBFT_EXPIRE_NUM) : 20; - - PARAMETER.agreeNodeCount = config.hasPath(ConfigKey.NODE_AGREE_NODE_COUNT) ? config - .getInt(ConfigKey.NODE_AGREE_NODE_COUNT) : MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; - PARAMETER.agreeNodeCount = PARAMETER.agreeNodeCount > MAX_ACTIVE_WITNESS_NUM - ? MAX_ACTIVE_WITNESS_NUM : PARAMETER.agreeNodeCount; - if (PARAMETER.isWitness()) { - // INSTANCE.agreeNodeCount = MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; + if (nc.getShutdownBlockHeight() >= 0) { + PARAMETER.shutdownBlockHeight = nc.getShutdownBlockHeight(); } - - PARAMETER.allowTvmFreeze = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_FREEZE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_FREEZE) : 0; - - PARAMETER.allowTvmVote = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_VOTE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_VOTE) : 0; - - PARAMETER.allowTvmLondon = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_LONDON) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_LONDON) : 0; - - PARAMETER.allowTvmCompatibleEvm = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM) : 0; - - PARAMETER.allowHigherLimitForMaxCpuTimeOfOneTx = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX) : 0; - - PARAMETER.allowNewRewardAlgorithm = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM) : 0; - - PARAMETER.allowOptimizedReturnValueOfChainId = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID) : 0; - - initBackupProperty(config); - initRocksDbBackupProperty(config); - initRocksDbSettings(config); - - PARAMETER.actuatorSet = - config.hasPath(ConfigKey.ACTUATOR_WHITELIST) - ? new HashSet<>(config.getStringList(ConfigKey.ACTUATOR_WHITELIST)) - : Collections.emptySet(); - - if (config.hasPath(ConfigKey.NODE_METRICS_ENABLE)) { - PARAMETER.nodeMetricsEnable = config.getBoolean(ConfigKey.NODE_METRICS_ENABLE); + if (nc.getShutdownBlockCount() >= 0) { + PARAMETER.shutdownBlockCount = nc.getShutdownBlockCount(); } + } - PARAMETER.metricsStorageEnable = config.hasPath(ConfigKey.METRICS_STORAGE_ENABLE) && config - .getBoolean(ConfigKey.METRICS_STORAGE_ENABLE); - PARAMETER.influxDbIp = config.hasPath(ConfigKey.METRICS_INFLUXDB_IP) ? config - .getString(ConfigKey.METRICS_INFLUXDB_IP) : Constant.LOCAL_HOST; - PARAMETER.influxDbPort = config.hasPath(ConfigKey.METRICS_INFLUXDB_PORT) ? config - .getInt(ConfigKey.METRICS_INFLUXDB_PORT) : 8086; - PARAMETER.influxDbDatabase = config.hasPath(ConfigKey.METRICS_INFLUXDB_DATABASE) ? config - .getString(ConfigKey.METRICS_INFLUXDB_DATABASE) : "metrics"; - PARAMETER.metricsReportInterval = config.hasPath(ConfigKey.METRICS_REPORT_INTERVAL) ? config - .getInt(ConfigKey.METRICS_REPORT_INTERVAL) : 10; - - PARAMETER.metricsPrometheusEnable = - config.hasPath(ConfigKey.METRICS_PROMETHEUS_ENABLE) - && config.getBoolean(ConfigKey.METRICS_PROMETHEUS_ENABLE); - PARAMETER.metricsPrometheusPort = config.hasPath(ConfigKey.METRICS_PROMETHEUS_PORT) ? config - .getInt(ConfigKey.METRICS_PROMETHEUS_PORT) : 9527; - PARAMETER.setOpenHistoryQueryWhenLiteFN( - config.hasPath(ConfigKey.NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN) - && config.getBoolean(ConfigKey.NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN)); - - PARAMETER.historyBalanceLookup = config.hasPath(ConfigKey.HISTORY_BALANCE_LOOKUP) && config - .getBoolean(ConfigKey.HISTORY_BALANCE_LOOKUP); - - if (config.hasPath(ConfigKey.OPEN_PRINT_LOG)) { - PARAMETER.openPrintLog = config.getBoolean(ConfigKey.OPEN_PRINT_LOG); + /** + * Apply platform-specific constraints after all config sources are resolved. + * ARM64 does not support LevelDB (native JNI library unavailable), + * so db.engine is forced to RocksDB regardless of config or CLI settings. + */ + private static void applyPlatformConstraints() { + if (Arch.isArm64() + && !Constant.ROCKSDB.equalsIgnoreCase(PARAMETER.storage.getDbEngine())) { + logger.warn("ARM64 only supports RocksDB, ignoring db.engine='{}'", + PARAMETER.storage.getDbEngine()); + PARAMETER.storage.setDbEngine(Constant.ROCKSDB); } + } - PARAMETER.openTransactionSort = config.hasPath(ConfigKey.OPEN_TRANSACTION_SORT) && config - .getBoolean(ConfigKey.OPEN_TRANSACTION_SORT); - - PARAMETER.allowAccountAssetOptimization = config - .hasPath(ConfigKey.ALLOW_ACCOUNT_ASSET_OPTIMIZATION) ? config - .getInt(ConfigKey.ALLOW_ACCOUNT_ASSET_OPTIMIZATION) : 0; - - PARAMETER.allowAssetOptimization = config - .hasPath(ConfigKey.ALLOW_ASSET_OPTIMIZATION) ? config - .getInt(ConfigKey.ALLOW_ASSET_OPTIMIZATION) : 0; + /** + * Apply parameters from config file. + */ + public static void applyConfigParams( + final Config config) { - PARAMETER.disabledApiList = - config.hasPath(ConfigKey.NODE_DISABLED_API_LIST) - ? config.getStringList(ConfigKey.NODE_DISABLED_API_LIST) - .stream().map(String::toLowerCase).collect(Collectors.toList()) - : Collections.emptyList(); + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + Wallet.setAddressPreFixString(Constant.ADD_PRE_FIX_STRING_MAINNET); - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)) { - try { - PARAMETER.shutdownBlockTime = new CronExpression(config.getString( - ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)); - } catch (ParseException e) { - throw new TronError(e, TronError.ErrCode.AUTO_STOP_PARAMS); - } - } + // crypto.engine handled by MiscConfig - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_HEIGHT)) { - PARAMETER.shutdownBlockHeight = config.getLong(ConfigKey.NODE_SHUTDOWN_BLOCK_HEIGHT); - } + // VM config: bind from config.conf "vm" section + vmConfig = VmConfig.fromConfig(config); + applyVmConfig(vmConfig); - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_COUNT)) { - PARAMETER.shutdownBlockCount = config.getLong(ConfigKey.NODE_SHUTDOWN_BLOCK_COUNT); - } + // Node config: bind from config.conf "node" section + nodeConfig = NodeConfig.fromConfig(config); + applyNodeConfig(nodeConfig); - if (config.hasPath(ConfigKey.BLOCK_CACHE_TIMEOUT)) { - PARAMETER.blockCacheTimeout = config.getLong(ConfigKey.BLOCK_CACHE_TIMEOUT); - } + // vm.minTimeRatio, vm.maxTimeRatio, vm.longRunningTime already handled by VmConfig above - if (config.hasPath(ConfigKey.ALLOW_NEW_REWARD)) { - PARAMETER.allowNewReward = config.getLong(ConfigKey.ALLOW_NEW_REWARD); - if (PARAMETER.allowNewReward > 1) { - PARAMETER.allowNewReward = 1; - } - if (PARAMETER.allowNewReward < 0) { - PARAMETER.allowNewReward = 0; - } - } + // Storage config: bind from config.conf "storage" section + PARAMETER.storage = new Storage(); + storageConfig = StorageConfig.fromConfig(config); + applyStorageConfig(storageConfig); - if (config.hasPath(ConfigKey.MEMO_FEE)) { - PARAMETER.memoFee = config.getLong(ConfigKey.MEMO_FEE); - if (PARAMETER.memoFee > 1_000_000_000) { - PARAMETER.memoFee = 1_000_000_000; - } - if (PARAMETER.memoFee < 0) { - PARAMETER.memoFee = 0; - } - } + // seed.node is a top-level config section (not under "node") — config structure + // is arguably misplaced, but preserved for backward compatibility - if (config.hasPath(ConfigKey.ALLOW_DELEGATE_OPTIMIZATION)) { - PARAMETER.allowDelegateOptimization = config.getLong(ConfigKey.ALLOW_DELEGATE_OPTIMIZATION); - PARAMETER.allowDelegateOptimization = min(PARAMETER.allowDelegateOptimization, 1, true); - PARAMETER.allowDelegateOptimization = max(PARAMETER.allowDelegateOptimization, 0, true); - } + // Genesis config: bind from config.conf "genesis.block" section + genesisConfig = GenesisConfig.fromConfig(config); + applyGenesisConfig(genesisConfig, config); - if (config.hasPath(ConfigKey.COMMITTEE_UNFREEZE_DELAY_DAYS)) { - PARAMETER.unfreezeDelayDays = config.getLong(ConfigKey.COMMITTEE_UNFREEZE_DELAY_DAYS); - if (PARAMETER.unfreezeDelayDays > 365) { - PARAMETER.unfreezeDelayDays = 365; - } - if (PARAMETER.unfreezeDelayDays < 0) { - PARAMETER.unfreezeDelayDays = 0; - } - } + // Block config: bind from config.conf "block" section + blockConfig = BlockConfig.fromConfig(config); + applyBlockConfig(blockConfig); - if (config.hasPath(ConfigKey.ALLOW_DYNAMIC_ENERGY)) { - PARAMETER.allowDynamicEnergy = config.getLong(ConfigKey.ALLOW_DYNAMIC_ENERGY); - PARAMETER.allowDynamicEnergy = min(PARAMETER.allowDynamicEnergy, 1, true); - PARAMETER.allowDynamicEnergy = max(PARAMETER.allowDynamicEnergy, 0, true); - } + // node discovery, legacy fallback, p2p, dns — all handled in applyNodeConfig - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_THRESHOLD)) { - PARAMETER.dynamicEnergyThreshold = config.getLong(ConfigKey.DYNAMIC_ENERGY_THRESHOLD); - PARAMETER.dynamicEnergyThreshold - = min(PARAMETER.dynamicEnergyThreshold, 100_000_000_000_000_000L, true); - PARAMETER.dynamicEnergyThreshold = max(PARAMETER.dynamicEnergyThreshold, 0, true); - } + // Misc config: storage, trx, energy — small domains, read via beans + miscConfig = MiscConfig.fromConfig(config); + applyMiscConfig(miscConfig); - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_INCREASE_FACTOR)) { - PARAMETER.dynamicEnergyIncreaseFactor - = config.getLong(ConfigKey.DYNAMIC_ENERGY_INCREASE_FACTOR); - PARAMETER.dynamicEnergyIncreaseFactor = - min(PARAMETER.dynamicEnergyIncreaseFactor, DYNAMIC_ENERGY_INCREASE_FACTOR_RANGE, true); - PARAMETER.dynamicEnergyIncreaseFactor = max(PARAMETER.dynamicEnergyIncreaseFactor, 0, true); - } + // vm, committee already handled above - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_MAX_FACTOR)) { - PARAMETER.dynamicEnergyMaxFactor - = config.getLong(ConfigKey.DYNAMIC_ENERGY_MAX_FACTOR); - PARAMETER.dynamicEnergyMaxFactor = - min(PARAMETER.dynamicEnergyMaxFactor, DYNAMIC_ENERGY_MAX_FACTOR_RANGE, true); - PARAMETER.dynamicEnergyMaxFactor = max(PARAMETER.dynamicEnergyMaxFactor, 0, true); - } + // Committee config: bind from config.conf "committee" section + committeeConfig = CommitteeConfig.fromConfig(config); + applyCommitteeConfig(committeeConfig); - PARAMETER.dynamicConfigEnable = config.hasPath(ConfigKey.DYNAMIC_CONFIG_ENABLE) - && config.getBoolean(ConfigKey.DYNAMIC_CONFIG_ENABLE); - if (config.hasPath(ConfigKey.DYNAMIC_CONFIG_CHECK_INTERVAL)) { - PARAMETER.dynamicConfigCheckInterval - = config.getLong(ConfigKey.DYNAMIC_CONFIG_CHECK_INTERVAL); - if (PARAMETER.dynamicConfigCheckInterval <= 0) { - PARAMETER.dynamicConfigCheckInterval = 600; - } - } else { - PARAMETER.dynamicConfigCheckInterval = 600; - } + // shielded transaction API, active/passive/fastForward — handled in applyNodeConfig - PARAMETER.allowTvmShangHai = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_SHANGHAI) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_SHANGHAI) : 0; - - PARAMETER.unsolidifiedBlockCheck = - config.hasPath(ConfigKey.UNSOLIDIFIED_BLOCK_CHECK) - && config.getBoolean(ConfigKey.UNSOLIDIFIED_BLOCK_CHECK); - - PARAMETER.maxUnsolidifiedBlocks = - config.hasPath(ConfigKey.MAX_UNSOLIDIFIED_BLOCKS) ? config - .getInt(ConfigKey.MAX_UNSOLIDIFIED_BLOCKS) : 54; - - long allowOldRewardOpt = config.hasPath(ConfigKey.COMMITTEE_ALLOW_OLD_REWARD_OPT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_OLD_REWARD_OPT) : 0; - if (allowOldRewardOpt == 1 && PARAMETER.allowNewRewardAlgorithm != 1 - && PARAMETER.allowNewReward != 1 && PARAMETER.allowTvmVote != 1) { - throw new IllegalArgumentException( - "At least one of the following proposals is required to be opened first: " - + "committee.allowNewRewardAlgorithm = 1" - + " or committee.allowNewReward = 1" - + " or committee.allowTvmVote = 1."); - } - PARAMETER.allowOldRewardOpt = allowOldRewardOpt; + // Rate limiter config: bind from config.conf "rate.limiter" section + rateLimiterConfig = RateLimiterConfig.fromConfig(config); + applyRateLimiterConfig(rateLimiterConfig); - PARAMETER.allowEnergyAdjustment = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ENERGY_ADJUSTMENT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ENERGY_ADJUSTMENT) : 0; + // Node backup: from NodeConfig bean + applyNodeBackupConfig(nodeConfig); - PARAMETER.allowStrictMath = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_STRICT_MATH) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_STRICT_MATH) : 0; + // actuatorSet already set in applyMiscConfig - PARAMETER.consensusLogicOptimization = - config.hasPath(ConfigKey.COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION) ? config - .getInt(ConfigKey.COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION) : 0; + // Metrics config: bind from config.conf "node.metrics" section + metricsConfig = MetricsConfig.fromConfig(config); + applyMetricsConfig(metricsConfig); - PARAMETER.allowTvmCancun = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_CANCUN) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_CANCUN) : 0; + // historyBalanceLookup already handled by MiscConfig above - PARAMETER.allowTvmBlob = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_BLOB) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_BLOB) : 0; + // node.shutdown — handled in applyNodeConfig - PARAMETER.allowTvmOsaka = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) : 0; + // Event config: bind from config.conf "event.subscribe" section + eventConfig = EventConfig.fromConfig(config); + applyEventConfig(eventConfig); logConfig(); } @@ -1198,26 +955,20 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { return; } - String witnessAddr = config.hasPath(ConfigKey.LOCAL_WITNESS_ACCOUNT_ADDRESS) - ? config.getString(ConfigKey.LOCAL_WITNESS_ACCOUNT_ADDRESS) : null; + LocalWitnessConfig lwConfig = LocalWitnessConfig.fromConfig(config); // path 2: config localwitness (private key list) - if (config.hasPath(ConfigKey.LOCAL_WITNESS)) { - List keys = config.getStringList(ConfigKey.LOCAL_WITNESS); - if (!keys.isEmpty()) { - localWitnesses = WitnessInitializer.initFromCFGPrivateKey(keys, witnessAddr); - return; - } + if (!lwConfig.getPrivateKeys().isEmpty()) { + localWitnesses = WitnessInitializer.initFromCFGPrivateKey( + lwConfig.getPrivateKeys(), lwConfig.getAccountAddress()); + return; } // path 3: config localwitnesskeystore + password - if (config.hasPath(ConfigKey.LOCAL_WITNESS_KEYSTORE)) { - List keystores = config.getStringList(ConfigKey.LOCAL_WITNESS_KEYSTORE); - if (!keystores.isEmpty()) { - localWitnesses = WitnessInitializer.initFromKeystore( - keystores, cmd.password, witnessAddr); - return; - } + if (!lwConfig.getKeystores().isEmpty()) { + localWitnesses = WitnessInitializer.initFromKeystore( + lwConfig.getKeystores(), cmd.password, lwConfig.getAccountAddress()); + return; } // no private key source configured @@ -1230,85 +981,35 @@ public static void clearParam() { CommonParameter.reset(); configFilePath = ""; localWitnesses = null; + nodeConfig = null; + vmConfig = null; + blockConfig = null; + committeeConfig = null; + storageConfig = null; + genesisConfig = null; + miscConfig = null; + rateLimiterConfig = null; + metricsConfig = null; + eventConfig = null; } - private static long getProposalExpirationTime(final Config config) { - if (config.hasPath(ConfigKey.COMMITTEE_PROPOSAL_EXPIRE_TIME)) { - throw new TronError("It is not allowed to configure committee.proposalExpireTime in " - + "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT); - } - if (config.hasPath(ConfigKey.BLOCK_PROPOSAL_EXPIRE_TIME)) { - long proposalExpireTime = config.getLong(ConfigKey.BLOCK_PROPOSAL_EXPIRE_TIME); - if (proposalExpireTime <= MIN_PROPOSAL_EXPIRE_TIME - || proposalExpireTime >= MAX_PROPOSAL_EXPIRE_TIME) { - throw new TronError("The value[block.proposalExpireTime] is only allowed to " - + "be greater than " + MIN_PROPOSAL_EXPIRE_TIME + " and less than " - + MAX_PROPOSAL_EXPIRE_TIME + "!", PARAMETER_INIT); - } - return proposalExpireTime; - } else { - return DEFAULT_PROPOSAL_EXPIRE_TIME; - } - } - - private static List getWitnessesFromConfig(final com.typesafe.config.Config config) { - return config.getObjectList(ConfigKey.GENESIS_BLOCK_WITNESSES).stream() - .map(Args::createWitness) - .collect(Collectors.toCollection(ArrayList::new)); - } - - private static Witness createWitness(final ConfigObject witnessAccount) { - final Witness witness = new Witness(); - witness.setAddress( - Commons.decodeFromBase58Check(witnessAccount.get("address").unwrapped().toString())); - witness.setUrl(witnessAccount.get("url").unwrapped().toString()); - witness.setVoteCount(witnessAccount.toConfig().getLong("voteCount")); - return witness; - } + // getProposalExpirationTime removed — logic moved to BlockConfig.fromConfig() - private static List getAccountsFromConfig(final com.typesafe.config.Config config) { - return config.getObjectList(ConfigKey.GENESIS_BLOCK_ASSETS).stream() - .map(Args::createAccount) - .collect(Collectors.toCollection(ArrayList::new)); - } + // getWitnessesFromConfig, createWitness, getAccountsFromConfig, createAccount + // removed — logic moved to applyGenesisConfig() - private static Account createAccount(final ConfigObject asset) { - final Account account = new Account(); - account.setAccountName(asset.get("accountName").unwrapped().toString()); - account.setAccountType(asset.get("accountType").unwrapped().toString()); - account.setAddress(Commons.decodeFromBase58Check(asset.get("address").unwrapped().toString())); - account.setBalance(asset.get("balance").unwrapped().toString()); - return account; - } + // getRateLimiterFromConfig removed — logic moved to applyRateLimiterConfig() - private static RateLimiterInitialization getRateLimiterFromConfig( - final com.typesafe.config.Config config) { - RateLimiterInitialization initialization = new RateLimiterInitialization(); - if (config.hasPath(ConfigKey.RATE_LIMITER_HTTP)) { - ArrayList list1 = config - .getObjectList(ConfigKey.RATE_LIMITER_HTTP).stream() - .map(RateLimiterInitialization::createHttpItem) - .collect(Collectors.toCollection(ArrayList::new)); - initialization.setHttpMap(list1); - } - if (config.hasPath(ConfigKey.RATE_LIMITER_RPC)) { - ArrayList list2 = config - .getObjectList(ConfigKey.RATE_LIMITER_RPC).stream() - .map(RateLimiterInitialization::createRpcItem) - .collect(Collectors.toCollection(ArrayList::new)); - initialization.setRpcMap(list2); - } - return initialization; - } + // getInetSocketAddress removed — use filterInetSocketAddress - public static List getInetSocketAddress( - final com.typesafe.config.Config config, String path, boolean filter) { + /** + * Parse and optionally filter a list of address strings. + * Overload that accepts a pre-read list from a bean instead of a config path. + */ + public static List filterInetSocketAddress( + List addressList, boolean filter) { List ret = new ArrayList<>(); - if (!config.hasPath(path)) { - return ret; - } - List list = config.getStringList(path); - for (String configString : list) { + for (String configString : addressList) { InetSocketAddress inetSocketAddress = NetUtil.parseInetSocketAddress(configString); if (filter) { String ip = inetSocketAddress.getAddress().getHostAddress(); @@ -1326,149 +1027,72 @@ public static List getInetSocketAddress( return ret; } - public static List getInetAddress( - final com.typesafe.config.Config config, String path) { - List ret = new ArrayList<>(); - if (!config.hasPath(path)) { - return ret; - } - List list = config.getStringList(path); - for (String configString : list) { - InetSocketAddress inetSocketAddress = NetUtil.parseInetSocketAddress(configString); - ret.add(inetSocketAddress.getAddress()); - } - return ret; - } + // getInetAddress removed — use filterInetSocketAddress - private static EventPluginConfig getEventPluginConfig( - final com.typesafe.config.Config config) { - EventPluginConfig eventPluginConfig = new EventPluginConfig(); + // getEventPluginConfig removed — logic moved to applyEventConfig() - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_VERSION)) { - eventPluginConfig.setVersion(config.getInt(ConfigKey.EVENT_SUBSCRIBE_VERSION)); - } - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM)) { - eventPluginConfig.setStartSyncBlockNum(config - .getLong(ConfigKey.EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM)); - } - - boolean useNativeQueue = false; - int bindPort = 0; - int sendQueueLength = 0; - if (config.hasPath(ConfigKey.USE_NATIVE_QUEUE)) { - useNativeQueue = config.getBoolean(ConfigKey.USE_NATIVE_QUEUE); - - if (config.hasPath(ConfigKey.NATIVE_QUEUE_BIND_PORT)) { - bindPort = config.getInt(ConfigKey.NATIVE_QUEUE_BIND_PORT); - } - - if (config.hasPath(ConfigKey.NATIVE_QUEUE_SEND_LENGTH)) { - sendQueueLength = config.getInt(ConfigKey.NATIVE_QUEUE_SEND_LENGTH); - } - - eventPluginConfig.setUseNativeQueue(useNativeQueue); - eventPluginConfig.setBindPort(bindPort); - eventPluginConfig.setSendQueueLength(sendQueueLength); - } - - // use event plugin - if (!useNativeQueue) { - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_PATH)) { - String pluginPath = config.getString(ConfigKey.EVENT_SUBSCRIBE_PATH); - if (StringUtils.isNotEmpty(pluginPath)) { - eventPluginConfig.setPluginPath(pluginPath.trim()); - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_SERVER)) { - String serverAddress = config.getString(ConfigKey.EVENT_SUBSCRIBE_SERVER); - if (StringUtils.isNotEmpty(serverAddress)) { - eventPluginConfig.setServerAddress(serverAddress.trim()); - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_DB_CONFIG)) { - String dbConfig = config.getString(ConfigKey.EVENT_SUBSCRIBE_DB_CONFIG); - if (StringUtils.isNotEmpty(dbConfig)) { - eventPluginConfig.setDbConfig(dbConfig.trim()); - } - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_TOPICS)) { - List triggerConfigList = config.getObjectList(ConfigKey.EVENT_SUBSCRIBE_TOPICS) - .stream() - .map(Args::createTriggerConfig) - .collect(Collectors.toCollection(ArrayList::new)); - - eventPluginConfig.setTriggerConfigList(triggerConfigList); - } - - return eventPluginConfig; - } - - - public static PublishConfig loadDnsPublishConfig(final com.typesafe.config.Config config) { + public static PublishConfig loadDnsPublishConfig(NodeConfig nodeConfig) { PublishConfig publishConfig = new PublishConfig(); - if (config.hasPath(ConfigKey.NODE_DNS_PUBLISH)) { - publishConfig.setDnsPublishEnable(config.getBoolean(ConfigKey.NODE_DNS_PUBLISH)); - } - loadDnsPublishParameters(config, publishConfig); + NodeConfig.DnsConfig dns = nodeConfig.getDns(); + publishConfig.setDnsPublishEnable(dns.isPublish()); + loadDnsPublishParameters(dns, publishConfig); return publishConfig; } + /** + * Load DNS publish parameters from bean into PublishConfig. + * Public method — called by tests and external code. + */ public static void loadDnsPublishParameters(final com.typesafe.config.Config config, PublishConfig publishConfig) { + NodeConfig nodeConfig = NodeConfig.fromConfig(config); + loadDnsPublishParameters(nodeConfig.getDns(), publishConfig); + } + + private static void loadDnsPublishParameters(NodeConfig.DnsConfig dns, + PublishConfig publishConfig) { + if (publishConfig.isDnsPublishEnable()) { - if (config.hasPath(ConfigKey.NODE_DNS_DOMAIN) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_DOMAIN))) { - publishConfig.setDnsDomain(config.getString(ConfigKey.NODE_DNS_DOMAIN)); + if (StringUtils.isNotEmpty(dns.getDnsDomain())) { + publishConfig.setDnsDomain(dns.getDnsDomain()); } else { - logEmptyError(ConfigKey.NODE_DNS_DOMAIN); + logEmptyError("node.dns.dnsDomain"); } - if (config.hasPath(ConfigKey.NODE_DNS_CHANGE_THRESHOLD)) { - double changeThreshold = config.getDouble(ConfigKey.NODE_DNS_CHANGE_THRESHOLD); - if (changeThreshold > 0) { - publishConfig.setChangeThreshold(changeThreshold); - } else { - logger.error("Check {}, should be bigger than 0, default 0.1", - ConfigKey.NODE_DNS_CHANGE_THRESHOLD); - } + if (dns.getChangeThreshold() > 0) { + publishConfig.setChangeThreshold(dns.getChangeThreshold()); + } else if (Double.compare(dns.getChangeThreshold(), 0.0) != 0) { + logger.error("Check node.dns.changeThreshold, should be bigger than 0, default 0.1"); } - if (config.hasPath(ConfigKey.NODE_DNS_MAX_MERGE_SIZE)) { - int maxMergeSize = config.getInt(ConfigKey.NODE_DNS_MAX_MERGE_SIZE); - if (maxMergeSize >= 1 && maxMergeSize <= 5) { - publishConfig.setMaxMergeSize(maxMergeSize); - } else { - logger.error("Check {}, should be [1~5], default 5", ConfigKey.NODE_DNS_MAX_MERGE_SIZE); - } + int maxMergeSize = dns.getMaxMergeSize(); + if (maxMergeSize >= 1 && maxMergeSize <= 5) { + publishConfig.setMaxMergeSize(maxMergeSize); + } else if (maxMergeSize != 0) { + logger.error("Check node.dns.maxMergeSize, should be [1~5], default 5"); } - if (config.hasPath(ConfigKey.NODE_DNS_PRIVATE) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_PRIVATE))) { - publishConfig.setDnsPrivate(config.getString(ConfigKey.NODE_DNS_PRIVATE)); + if (StringUtils.isNotEmpty(dns.getDnsPrivate())) { + publishConfig.setDnsPrivate(dns.getDnsPrivate()); } else { - logEmptyError(ConfigKey.NODE_DNS_PRIVATE); + logEmptyError("node.dns.dnsPrivate"); } - if (config.hasPath(ConfigKey.NODE_DNS_KNOWN_URLS)) { - publishConfig.setKnownTreeUrls(config.getStringList(ConfigKey.NODE_DNS_KNOWN_URLS)); + if (!dns.getKnownUrls().isEmpty()) { + publishConfig.setKnownTreeUrls(dns.getKnownUrls()); } - if (config.hasPath(ConfigKey.NODE_DNS_STATIC_NODES)) { + if (!dns.getStaticNodes().isEmpty()) { publishConfig.setStaticNodes( - getInetSocketAddress(config, ConfigKey.NODE_DNS_STATIC_NODES, false)); + filterInetSocketAddress(dns.getStaticNodes(), false)); } - if (config.hasPath(ConfigKey.NODE_DNS_SERVER_TYPE) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_SERVER_TYPE))) { - String serverType = config.getString(ConfigKey.NODE_DNS_SERVER_TYPE); + String serverType = dns.getServerType(); + if (StringUtils.isNotEmpty(serverType)) { if (!"aws".equalsIgnoreCase(serverType) && !"aliyun".equalsIgnoreCase(serverType)) { throw new IllegalArgumentException( - String.format("Check %s, must be aws or aliyun", ConfigKey.NODE_DNS_SERVER_TYPE)); + "Check node.dns.serverType, must be aws or aliyun"); } if ("aws".equalsIgnoreCase(serverType)) { publishConfig.setDnsType(DnsType.AwsRoute53); @@ -1476,38 +1100,34 @@ public static void loadDnsPublishParameters(final com.typesafe.config.Config con publishConfig.setDnsType(DnsType.AliYun); } } else { - logEmptyError(ConfigKey.NODE_DNS_SERVER_TYPE); + logEmptyError("node.dns.serverType"); } - if (config.hasPath(ConfigKey.NODE_DNS_ACCESS_KEY_ID) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_ID))) { - publishConfig.setAccessKeyId(config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_ID)); + if (StringUtils.isNotEmpty(dns.getAccessKeyId())) { + publishConfig.setAccessKeyId(dns.getAccessKeyId()); } else { - logEmptyError(ConfigKey.NODE_DNS_ACCESS_KEY_ID); + logEmptyError("node.dns.accessKeyId"); } - if (config.hasPath(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET))) { - publishConfig.setAccessKeySecret(config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET)); + if (StringUtils.isNotEmpty(dns.getAccessKeySecret())) { + publishConfig.setAccessKeySecret(dns.getAccessKeySecret()); } else { - logEmptyError(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET); + logEmptyError("node.dns.accessKeySecret"); } if (publishConfig.getDnsType() == DnsType.AwsRoute53) { - if (config.hasPath(ConfigKey.NODE_DNS_AWS_REGION) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_AWS_REGION))) { - publishConfig.setAwsRegion(config.getString(ConfigKey.NODE_DNS_AWS_REGION)); + if (StringUtils.isNotEmpty(dns.getAwsRegion())) { + publishConfig.setAwsRegion(dns.getAwsRegion()); } else { - logEmptyError(ConfigKey.NODE_DNS_AWS_REGION); + logEmptyError("node.dns.awsRegion"); } - if (config.hasPath(ConfigKey.NODE_DNS_AWS_HOST_ZONE_ID)) { - publishConfig.setAwsHostZoneId(config.getString(ConfigKey.NODE_DNS_AWS_HOST_ZONE_ID)); + if (StringUtils.isNotEmpty(dns.getAwsHostZoneId())) { + publishConfig.setAwsHostZoneId(dns.getAwsHostZoneId()); } } else { - if (config.hasPath(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT))) { - publishConfig.setAliDnsEndpoint(config.getString(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT)); + if (StringUtils.isNotEmpty(dns.getAliyunDnsEndpoint())) { + publishConfig.setAliDnsEndpoint(dns.getAliyunDnsEndpoint()); } else { - logEmptyError(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT); + logEmptyError("node.dns.aliyunDnsEndpoint"); } } } @@ -1517,80 +1137,13 @@ private static void logEmptyError(String arg) { throw new IllegalArgumentException(String.format("Check %s, must not be null or empty", arg)); } - private static TriggerConfig createTriggerConfig(ConfigObject triggerObject) { - if (Objects.isNull(triggerObject)) { - return null; - } - - TriggerConfig triggerConfig = new TriggerConfig(); - - String triggerName = triggerObject.get("triggerName").unwrapped().toString(); - triggerConfig.setTriggerName(triggerName); - - String enabled = triggerObject.get("enable").unwrapped().toString(); - triggerConfig.setEnabled("true".equalsIgnoreCase(enabled)); - - String topic = triggerObject.get("topic").unwrapped().toString(); - triggerConfig.setTopic(topic); - - if (triggerObject.containsKey("redundancy")) { - String redundancy = triggerObject.get("redundancy").unwrapped().toString(); - triggerConfig.setRedundancy("true".equalsIgnoreCase(redundancy)); - } - - if (triggerObject.containsKey("ethCompatible")) { - String ethCompatible = triggerObject.get("ethCompatible").unwrapped().toString(); - triggerConfig.setEthCompatible("true".equalsIgnoreCase(ethCompatible)); - } - - if (triggerObject.containsKey("solidified")) { - String solidified = triggerObject.get("solidified").unwrapped().toString(); - triggerConfig.setSolidified("true".equalsIgnoreCase(solidified)); - } - - return triggerConfig; - } - - private static FilterQuery getEventFilter(final com.typesafe.config.Config config) { - FilterQuery filter = new FilterQuery(); - long fromBlockLong = 0; - long toBlockLong = 0; + // createTriggerConfig removed — logic moved to applyEventConfig() + // getEventFilter removed — logic moved to applyEventConfig() - String fromBlock = config.getString(ConfigKey.EVENT_SUBSCRIBE_FROM_BLOCK).trim(); - try { - fromBlockLong = FilterQuery.parseFromBlockNumber(fromBlock); - } catch (Exception e) { - logger.error("invalid filter: fromBlockNumber: {}", fromBlock, e); - return null; - } - filter.setFromBlock(fromBlockLong); - - String toBlock = config.getString(ConfigKey.EVENT_SUBSCRIBE_TO_BLOCK).trim(); - try { - toBlockLong = FilterQuery.parseToBlockNumber(toBlock); - } catch (Exception e) { - logger.error("invalid filter: toBlockNumber: {}", toBlock, e); - return null; - } - filter.setToBlock(toBlockLong); - - List addressList = config.getStringList(ConfigKey.EVENT_SUBSCRIBE_CONTRACT_ADDRESS); - addressList = addressList.stream().filter(address -> StringUtils.isNotEmpty(address)).collect( - Collectors.toList()); - filter.setContractAddressList(addressList); - - List topicList = config.getStringList(ConfigKey.EVENT_SUBSCRIBE_CONTRACT_TOPIC); - topicList = topicList.stream().filter(top -> StringUtils.isNotEmpty(top)).collect( - Collectors.toList()); - filter.setContractTopicList(topicList); - - return filter; - } - - private static void externalIp(final com.typesafe.config.Config config) { - if (!config.hasPath(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP) || config - .getString(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP).trim().isEmpty()) { - if (PARAMETER.nodeExternalIp == null) { + private static void externalIp(NodeConfig nodeConfig) { + String externalIp = nodeConfig.getDiscoveryExternalIp(); + if (StringUtils.isEmpty(externalIp)) { + if (StringUtils.isEmpty(PARAMETER.nodeExternalIp)) { logger.info("External IP wasn't set, using ipv4 from libp2p"); PARAMETER.nodeExternalIp = PARAMETER.p2pConfig.getIp(); if (StringUtils.isEmpty(PARAMETER.nodeExternalIp)) { @@ -1598,69 +1151,12 @@ private static void externalIp(final com.typesafe.config.Config config) { } } } else { - PARAMETER.nodeExternalIp = config.getString(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP).trim(); + PARAMETER.nodeExternalIp = externalIp; } } - private static void initRocksDbSettings(Config config) { - String prefix = ConfigKey.STORAGE_DB_SETTING; - int levelNumber = config.hasPath(prefix + "levelNumber") - ? config.getInt(prefix + "levelNumber") : 7; - int compactThreads = config.hasPath(prefix + "compactThreads") - ? config.getInt(prefix + "compactThreads") - : max(Runtime.getRuntime().availableProcessors(), 1, true); - int blocksize = config.hasPath(prefix + "blocksize") - ? config.getInt(prefix + "blocksize") : 16; - long maxBytesForLevelBase = config.hasPath(prefix + "maxBytesForLevelBase") - ? config.getInt(prefix + "maxBytesForLevelBase") : 256; - double maxBytesForLevelMultiplier = config.hasPath(prefix + "maxBytesForLevelMultiplier") - ? config.getDouble(prefix + "maxBytesForLevelMultiplier") : 10; - int level0FileNumCompactionTrigger = - config.hasPath(prefix + "level0FileNumCompactionTrigger") ? config - .getInt(prefix + "level0FileNumCompactionTrigger") : 2; - long targetFileSizeBase = config.hasPath(prefix + "targetFileSizeBase") ? config - .getLong(prefix + "targetFileSizeBase") : 64; - int targetFileSizeMultiplier = config.hasPath(prefix + "targetFileSizeMultiplier") ? config - .getInt(prefix + "targetFileSizeMultiplier") : 1; - int maxOpenFiles = config.hasPath(prefix + "maxOpenFiles") - ? config.getInt(prefix + "maxOpenFiles") : 5000; - - PARAMETER.rocksDBCustomSettings = RocksDbSettings - .initCustomSettings(levelNumber, compactThreads, blocksize, maxBytesForLevelBase, - maxBytesForLevelMultiplier, level0FileNumCompactionTrigger, - targetFileSizeBase, targetFileSizeMultiplier, maxOpenFiles); - RocksDbSettings.loggingSettings(); - } - - private static void initRocksDbBackupProperty(Config config) { - boolean enable = - config.hasPath(ConfigKey.STORAGE_BACKUP_ENABLE) - && config.getBoolean(ConfigKey.STORAGE_BACKUP_ENABLE); - String propPath = config.hasPath(ConfigKey.STORAGE_BACKUP_PROP_PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_PROP_PATH) : "prop.properties"; - String bak1path = config.hasPath(ConfigKey.STORAGE_BACKUP_BAK1PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_BAK1PATH) : "bak1/database/"; - String bak2path = config.hasPath(ConfigKey.STORAGE_BACKUP_BAK2PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_BAK2PATH) : "bak2/database/"; - int frequency = config.hasPath(ConfigKey.STORAGE_BACKUP_FREQUENCY) - ? config.getInt(ConfigKey.STORAGE_BACKUP_FREQUENCY) : 10000; - PARAMETER.dbBackupConfig = DbBackupConfig.getInstance() - .initArgs(enable, propPath, bak1path, bak2path, frequency); - } - - private static void initBackupProperty(Config config) { - PARAMETER.backupPriority = config.hasPath(ConfigKey.NODE_BACKUP_PRIORITY) - ? config.getInt(ConfigKey.NODE_BACKUP_PRIORITY) : 0; - - PARAMETER.backupPort = config.hasPath(ConfigKey.NODE_BACKUP_PORT) - ? config.getInt(ConfigKey.NODE_BACKUP_PORT) : 10001; - - PARAMETER.keepAliveInterval = config.hasPath(ConfigKey.NODE_BACKUP_KEEPALIVEINTERVAL) - ? config.getInt(ConfigKey.NODE_BACKUP_KEEPALIVEINTERVAL) : 3000; - - PARAMETER.backupMembers = config.hasPath(ConfigKey.NODE_BACKUP_MEMBERS) - ? config.getStringList(ConfigKey.NODE_BACKUP_MEMBERS) : new ArrayList<>(); - } + // initRocksDbSettings, initRocksDbBackupProperty, initBackupProperty + // removed — logic moved to applyStorageConfig() and applyNodeBackupConfig() public static void logConfig() { CommonParameter parameter = CommonParameter.getInstance(); diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java deleted file mode 100644 index b21c9c440a4..00000000000 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ /dev/null @@ -1,331 +0,0 @@ -package org.tron.core.config.args; - -/** - * HOCON configuration key constants. - * These map to paths in config files (e.g. config.conf) and are read by Args.setParam(). - */ -final class ConfigKey { - - private ConfigKey() { - } - - // local witness - public static final String LOCAL_WITNESS = "localwitness"; // private key - public static final String LOCAL_WITNESS_ACCOUNT_ADDRESS = "localWitnessAccountAddress"; - public static final String LOCAL_WITNESS_KEYSTORE = "localwitnesskeystore"; - - // crypto - public static final String CRYPTO_ENGINE = "crypto.engine"; - - // vm - public static final String VM_SUPPORT_CONSTANT = "vm.supportConstant"; - public static final String VM_MAX_ENERGY_LIMIT_FOR_CONSTANT = "vm.maxEnergyLimitForConstant"; - public static final String VM_LRU_CACHE_SIZE = "vm.lruCacheSize"; - public static final String VM_MIN_TIME_RATIO = "vm.minTimeRatio"; - public static final String VM_MAX_TIME_RATIO = "vm.maxTimeRatio"; - public static final String VM_LONG_RUNNING_TIME = "vm.longRunningTime"; - public static final String VM_ESTIMATE_ENERGY = "vm.estimateEnergy"; - public static final String VM_ESTIMATE_ENERGY_MAX_RETRY = "vm.estimateEnergyMaxRetry"; - public static final String VM_TRACE = "vm.vmTrace"; - public static final String VM_SAVE_INTERNAL_TX = "vm.saveInternalTx"; - public static final String VM_SAVE_FEATURED_INTERNAL_TX = "vm.saveFeaturedInternalTx"; - public static final String VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS = - "vm.saveCancelAllUnfreezeV2Details"; - - // genesis - public static final String GENESIS_BLOCK = "genesis.block"; - public static final String GENESIS_BLOCK_TIMESTAMP = "genesis.block.timestamp"; - public static final String GENESIS_BLOCK_PARENTHASH = "genesis.block.parentHash"; - public static final String GENESIS_BLOCK_ASSETS = "genesis.block.assets"; - public static final String GENESIS_BLOCK_WITNESSES = "genesis.block.witnesses"; - - // block - public static final String BLOCK_NEED_SYNC_CHECK = "block.needSyncCheck"; - public static final String BLOCK_MAINTENANCE_TIME_INTERVAL = "block.maintenanceTimeInterval"; - public static final String BLOCK_PROPOSAL_EXPIRE_TIME = "block.proposalExpireTime"; - public static final String BLOCK_CHECK_FROZEN_TIME = "block.checkFrozenTime"; - public static final String BLOCK_CACHE_TIMEOUT = "node.blockCacheTimeout"; - - // node - discovery - public static final String NODE_DISCOVERY_ENABLE = "node.discovery.enable"; - public static final String NODE_DISCOVERY_PERSIST = "node.discovery.persist"; - public static final String NODE_DISCOVERY_EXTERNAL_IP = "node.discovery.external.ip"; - - // node - connection - public static final String NODE_EFFECTIVE_CHECK_ENABLE = "node.effectiveCheckEnable"; - public static final String NODE_CONNECTION_TIMEOUT = "node.connection.timeout"; - public static final String NODE_FETCH_BLOCK_TIMEOUT = "node.fetchBlock.timeout"; - public static final String NODE_CHANNEL_READ_TIMEOUT = "node.channel.read.timeout"; - public static final String NODE_MAX_CONNECTIONS = "node.maxConnections"; - public static final String NODE_MIN_CONNECTIONS = "node.minConnections"; - public static final String NODE_MIN_ACTIVE_CONNECTIONS = "node.minActiveConnections"; - public static final String NODE_MAX_CONNECTIONS_WITH_SAME_IP = "node.maxConnectionsWithSameIp"; - public static final String NODE_MIN_PARTICIPATION_RATE = "node.minParticipationRate"; - public static final String NODE_MAX_ACTIVE_NODES = "node.maxActiveNodes"; - public static final String NODE_MAX_ACTIVE_NODES_WITH_SAME_IP = "node.maxActiveNodesWithSameIp"; - public static final String NODE_CONNECT_FACTOR = "node.connectFactor"; - public static final String NODE_ACTIVE_CONNECT_FACTOR = "node.activeConnectFactor"; - public static final String NODE_IS_OPEN_FULL_TCP_DISCONNECT = "node.isOpenFullTcpDisconnect"; - public static final String NODE_INACTIVE_THRESHOLD = "node.inactiveThreshold"; - public static final String NODE_DETECT_ENABLE = "node.nodeDetectEnable"; - public static final String NODE_MAX_HTTP_CONNECT_NUMBER = "node.maxHttpConnectNumber"; - - // node - p2p - public static final String NODE_LISTEN_PORT = "node.listen.port"; - public static final String NODE_P2P_VERSION = "node.p2p.version"; - public static final String NODE_ENABLE_IPV6 = "node.enableIpv6"; - public static final String NODE_SYNC_FETCH_BATCH_NUM = "node.syncFetchBatchNum"; - public static final String NODE_MAX_TPS = "node.maxTps"; - public static final String NODE_NET_MAX_TRX_PER_SECOND = "node.netMaxTrxPerSecond"; - public static final String NODE_TCP_NETTY_WORK_THREAD_NUM = "node.tcpNettyWorkThreadNum"; - public static final String NODE_UDP_NETTY_WORK_THREAD_NUM = "node.udpNettyWorkThreadNum"; - public static final String NODE_VALIDATE_SIGN_THREAD_NUM = "node.validateSignThreadNum"; - public static final String NODE_RECEIVE_TCP_MIN_DATA_LENGTH = "node.receiveTcpMinDataLength"; - public static final String NODE_PRODUCED_TIMEOUT = "node.blockProducedTimeOut"; - public static final String NODE_MAX_TRANSACTION_PENDING_SIZE = "node.maxTransactionPendingSize"; - public static final String NODE_PENDING_TRANSACTION_TIMEOUT = "node.pendingTransactionTimeout"; - public static final String NODE_ACTIVE = "node.active"; - public static final String NODE_PASSIVE = "node.passive"; - public static final String NODE_FAST_FORWARD = "node.fastForward"; - public static final String NODE_MAX_FAST_FORWARD_NUM = "node.maxFastForwardNum"; - public static final String NODE_AGREE_NODE_COUNT = "node.agreeNodeCount"; - public static final String NODE_SOLIDITY_THREADS = "node.solidity.threads"; - public static final String NODE_TRUST_NODE = "node.trustNode"; - public static final String NODE_WALLET_EXTENSION_API = "node.walletExtensionApi"; - public static final String NODE_VALID_CONTRACT_PROTO_THREADS = "node.validContractProto.threads"; - public static final String NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS = - "node.shieldedTransInPendingMaxCounts"; - public static final String NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION = - "node.fullNodeAllowShieldedTransaction"; - public static final String ALLOW_SHIELDED_TRANSACTION_API = - "node.allowShieldedTransactionApi"; - public static final String NODE_ZEN_TOKENID = "node.zenTokenId"; - public static final String NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN = - "node.openHistoryQueryWhenLiteFN"; - public static final String NODE_METRICS_ENABLE = "node.metricsEnable"; - public static final String NODE_DISABLED_API_LIST = "node.disabledApi"; - - // node - rpc - public static final String NODE_RPC_PORT = "node.rpc.port"; - public static final String NODE_RPC_SOLIDITY_PORT = "node.rpc.solidityPort"; - public static final String NODE_RPC_PBFT_PORT = "node.rpc.PBFTPort"; - public static final String NODE_RPC_ENABLE = "node.rpc.enable"; - public static final String NODE_RPC_SOLIDITY_ENABLE = "node.rpc.solidityEnable"; - public static final String NODE_RPC_PBFT_ENABLE = "node.rpc.PBFTEnable"; - public static final String NODE_RPC_THREAD = "node.rpc.thread"; - public static final String NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION = - "node.rpc.maxConcurrentCallsPerConnection"; - public static final String NODE_RPC_FLOW_CONTROL_WINDOW = "node.rpc.flowControlWindow"; - public static final String NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS = - "node.rpc.maxConnectionIdleInMillis"; - public static final String NODE_RPC_MAX_RST_STREAM = "node.rpc.maxRstStream"; - public static final String NODE_RPC_SECONDS_PER_WINDOW = "node.rpc.secondsPerWindow"; - public static final String NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS = - "node.rpc.maxConnectionAgeInMillis"; - public static final String NODE_RPC_MAX_MESSAGE_SIZE = "node.rpc.maxMessageSize"; - public static final String NODE_RPC_MAX_HEADER_LIST_SIZE = "node.rpc.maxHeaderListSize"; - public static final String NODE_RPC_REFLECTION_SERVICE = "node.rpc.reflectionService"; - public static final String NODE_RPC_MIN_EFFECTIVE_CONNECTION = - "node.rpc.minEffectiveConnection"; - public static final String NODE_RPC_TRX_CACHE_ENABLE = "node.rpc.trxCacheEnable"; - - // node - http - public static final String NODE_HTTP_FULLNODE_PORT = "node.http.fullNodePort"; - public static final String NODE_HTTP_SOLIDITY_PORT = "node.http.solidityPort"; - public static final String NODE_HTTP_FULLNODE_ENABLE = "node.http.fullNodeEnable"; - public static final String NODE_HTTP_SOLIDITY_ENABLE = "node.http.solidityEnable"; - public static final String NODE_HTTP_PBFT_ENABLE = "node.http.PBFTEnable"; - public static final String NODE_HTTP_PBFT_PORT = "node.http.PBFTPort"; - - // node - jsonrpc - public static final String NODE_JSONRPC_HTTP_FULLNODE_ENABLE = - "node.jsonrpc.httpFullNodeEnable"; - public static final String NODE_JSONRPC_HTTP_FULLNODE_PORT = "node.jsonrpc.httpFullNodePort"; - public static final String NODE_JSONRPC_HTTP_SOLIDITY_ENABLE = - "node.jsonrpc.httpSolidityEnable"; - public static final String NODE_JSONRPC_HTTP_SOLIDITY_PORT = "node.jsonrpc.httpSolidityPort"; - public static final String NODE_JSONRPC_HTTP_PBFT_ENABLE = "node.jsonrpc.httpPBFTEnable"; - public static final String NODE_JSONRPC_HTTP_PBFT_PORT = "node.jsonrpc.httpPBFTPort"; - public static final String NODE_JSONRPC_MAX_BLOCK_RANGE = "node.jsonrpc.maxBlockRange"; - public static final String NODE_JSONRPC_MAX_SUB_TOPICS = "node.jsonrpc.maxSubTopics"; - public static final String NODE_JSONRPC_MAX_BLOCK_FILTER_NUM = - "node.jsonrpc.maxBlockFilterNum"; - - // node - dns - public static final String NODE_DNS_TREE_URLS = "node.dns.treeUrls"; - public static final String NODE_DNS_PUBLISH = "node.dns.publish"; - public static final String NODE_DNS_DOMAIN = "node.dns.dnsDomain"; - public static final String NODE_DNS_CHANGE_THRESHOLD = "node.dns.changeThreshold"; - public static final String NODE_DNS_MAX_MERGE_SIZE = "node.dns.maxMergeSize"; - public static final String NODE_DNS_PRIVATE = "node.dns.dnsPrivate"; - public static final String NODE_DNS_KNOWN_URLS = "node.dns.knownUrls"; - public static final String NODE_DNS_STATIC_NODES = "node.dns.staticNodes"; - public static final String NODE_DNS_SERVER_TYPE = "node.dns.serverType"; - public static final String NODE_DNS_ACCESS_KEY_ID = "node.dns.accessKeyId"; - public static final String NODE_DNS_ACCESS_KEY_SECRET = "node.dns.accessKeySecret"; - public static final String NODE_DNS_ALIYUN_ENDPOINT = "node.dns.aliyunDnsEndpoint"; - public static final String NODE_DNS_AWS_REGION = "node.dns.awsRegion"; - public static final String NODE_DNS_AWS_HOST_ZONE_ID = "node.dns.awsHostZoneId"; - - // node - backup - public static final String NODE_BACKUP_PRIORITY = "node.backup.priority"; - public static final String NODE_BACKUP_PORT = "node.backup.port"; - public static final String NODE_BACKUP_KEEPALIVEINTERVAL = "node.backup.keepAliveInterval"; - public static final String NODE_BACKUP_MEMBERS = "node.backup.members"; - - // node - shutdown - public static final String NODE_SHUTDOWN_BLOCK_TIME = "node.shutdown.BlockTime"; - public static final String NODE_SHUTDOWN_BLOCK_HEIGHT = "node.shutdown.BlockHeight"; - public static final String NODE_SHUTDOWN_BLOCK_COUNT = "node.shutdown.BlockCount"; - - // node - dynamic config - public static final String DYNAMIC_CONFIG_ENABLE = "node.dynamicConfig.enable"; - public static final String DYNAMIC_CONFIG_CHECK_INTERVAL = "node.dynamicConfig.checkInterval"; - - // node - unsolidified - public static final String UNSOLIDIFIED_BLOCK_CHECK = "node.unsolidifiedBlockCheck"; - public static final String MAX_UNSOLIDIFIED_BLOCKS = "node.maxUnsolidifiedBlocks"; - - // node - misc - public static final String OPEN_PRINT_LOG = "node.openPrintLog"; - public static final String OPEN_TRANSACTION_SORT = "node.openTransactionSort"; - - // committee - public static final String COMMITTEE_ALLOW_CREATION_OF_CONTRACTS = - "committee.allowCreationOfContracts"; - public static final String COMMITTEE_ALLOW_MULTI_SIGN = "committee.allowMultiSign"; - public static final String COMMITTEE_ALLOW_ADAPTIVE_ENERGY = "committee.allowAdaptiveEnergy"; - public static final String COMMITTEE_ALLOW_DELEGATE_RESOURCE = - "committee.allowDelegateResource"; - public static final String COMMITTEE_ALLOW_SAME_TOKEN_NAME = "committee.allowSameTokenName"; - public static final String COMMITTEE_ALLOW_TVM_TRANSFER_TRC10 = - "committee.allowTvmTransferTrc10"; - public static final String COMMITTEE_ALLOW_TVM_CONSTANTINOPLE = - "committee.allowTvmConstantinople"; - public static final String COMMITTEE_ALLOW_TVM_SOLIDITY059 = "committee.allowTvmSolidity059"; - public static final String COMMITTEE_FORBID_TRANSFER_TO_CONTRACT = - "committee.forbidTransferToContract"; - public static final String COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION = - "committee.allowShieldedTRC20Transaction"; - public static final String COMMITTEE_ALLOW_TVM_ISTANBUL = "committee.allowTvmIstanbul"; - public static final String COMMITTEE_ALLOW_MARKET_TRANSACTION = - "committee.allowMarketTransaction"; - public static final String COMMITTEE_ALLOW_PROTO_FILTER_NUM = - "committee.allowProtoFilterNum"; - public static final String COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT = - "committee.allowAccountStateRoot"; - public static final String COMMITTEE_ALLOW_PBFT = "committee.allowPBFT"; - public static final String COMMITTEE_PBFT_EXPIRE_NUM = "committee.pBFTExpireNum"; - public static final String COMMITTEE_ALLOW_TRANSACTION_FEE_POOL = - "committee.allowTransactionFeePool"; - public static final String COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION = - "committee.allowBlackHoleOptimization"; - public static final String COMMITTEE_ALLOW_NEW_RESOURCE_MODEL = - "committee.allowNewResourceModel"; - public static final String COMMITTEE_ALLOW_RECEIPTS_MERKLE_ROOT = - "committee.allowReceiptsMerkleRoot"; - public static final String COMMITTEE_ALLOW_TVM_FREEZE = "committee.allowTvmFreeze"; - public static final String COMMITTEE_ALLOW_TVM_VOTE = "committee.allowTvmVote"; - public static final String COMMITTEE_UNFREEZE_DELAY_DAYS = "committee.unfreezeDelayDays"; - public static final String COMMITTEE_ALLOW_TVM_LONDON = "committee.allowTvmLondon"; - public static final String COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM = - "committee.allowTvmCompatibleEvm"; - public static final String COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX = - "committee.allowHigherLimitForMaxCpuTimeOfOneTx"; - public static final String COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM = - "committee.allowNewRewardAlgorithm"; - public static final String COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID = - "committee.allowOptimizedReturnValueOfChainId"; - public static final String COMMITTEE_CHANGED_DELEGATION = "committee.changedDelegation"; - public static final String COMMITTEE_ALLOW_TVM_SHANGHAI = "committee.allowTvmShangHai"; - public static final String COMMITTEE_ALLOW_OLD_REWARD_OPT = "committee.allowOldRewardOpt"; - public static final String COMMITTEE_ALLOW_ENERGY_ADJUSTMENT = - "committee.allowEnergyAdjustment"; - public static final String COMMITTEE_ALLOW_STRICT_MATH = "committee.allowStrictMath"; - public static final String COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION = - "committee.consensusLogicOptimization"; - public static final String COMMITTEE_ALLOW_TVM_CANCUN = "committee.allowTvmCancun"; - public static final String COMMITTEE_ALLOW_TVM_BLOB = "committee.allowTvmBlob"; - public static final String COMMITTEE_PROPOSAL_EXPIRE_TIME = "committee.proposalExpireTime"; - public static final String COMMITTEE_ALLOW_TVM_OSAKA = "committee.allowTvmOsaka"; - public static final String ALLOW_ACCOUNT_ASSET_OPTIMIZATION = - "committee.allowAccountAssetOptimization"; - public static final String ALLOW_ASSET_OPTIMIZATION = "committee.allowAssetOptimization"; - public static final String ALLOW_NEW_REWARD = "committee.allowNewReward"; - public static final String MEMO_FEE = "committee.memoFee"; - public static final String ALLOW_DELEGATE_OPTIMIZATION = - "committee.allowDelegateOptimization"; - public static final String ALLOW_DYNAMIC_ENERGY = "committee.allowDynamicEnergy"; - public static final String DYNAMIC_ENERGY_THRESHOLD = "committee.dynamicEnergyThreshold"; - public static final String DYNAMIC_ENERGY_INCREASE_FACTOR = - "committee.dynamicEnergyIncreaseFactor"; - public static final String DYNAMIC_ENERGY_MAX_FACTOR = "committee.dynamicEnergyMaxFactor"; - - // storage - public static final String STORAGE_NEEDTO_UPDATE_ASSET = "storage.needToUpdateAsset"; - public static final String STORAGE_BACKUP_ENABLE = "storage.backup.enable"; - public static final String STORAGE_BACKUP_PROP_PATH = "storage.backup.propPath"; - public static final String STORAGE_BACKUP_BAK1PATH = "storage.backup.bak1path"; - public static final String STORAGE_BACKUP_BAK2PATH = "storage.backup.bak2path"; - public static final String STORAGE_BACKUP_FREQUENCY = "storage.backup.frequency"; - public static final String STORAGE_DB_SETTING = "storage.dbSettings."; - public static final String HISTORY_BALANCE_LOOKUP = "storage.balance.history.lookup"; - - // event - public static final String EVENT_SUBSCRIBE = "event.subscribe"; - public static final String EVENT_SUBSCRIBE_ENABLE = "event.subscribe.enable"; - public static final String EVENT_SUBSCRIBE_FILTER = "event.subscribe.filter"; - public static final String EVENT_SUBSCRIBE_VERSION = "event.subscribe.version"; - public static final String EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM = - "event.subscribe.startSyncBlockNum"; - public static final String EVENT_SUBSCRIBE_PATH = "event.subscribe.path"; - public static final String EVENT_SUBSCRIBE_SERVER = "event.subscribe.server"; - public static final String EVENT_SUBSCRIBE_DB_CONFIG = "event.subscribe.dbconfig"; - public static final String EVENT_SUBSCRIBE_TOPICS = "event.subscribe.topics"; - public static final String EVENT_SUBSCRIBE_FROM_BLOCK = "event.subscribe.filter.fromblock"; - public static final String EVENT_SUBSCRIBE_TO_BLOCK = "event.subscribe.filter.toblock"; - public static final String EVENT_SUBSCRIBE_CONTRACT_ADDRESS = - "event.subscribe.filter.contractAddress"; - public static final String EVENT_SUBSCRIBE_CONTRACT_TOPIC = - "event.subscribe.filter.contractTopic"; - public static final String USE_NATIVE_QUEUE = "event.subscribe.native.useNativeQueue"; - public static final String NATIVE_QUEUE_BIND_PORT = "event.subscribe.native.bindport"; - public static final String NATIVE_QUEUE_SEND_LENGTH = - "event.subscribe.native.sendqueuelength"; - - // rate limiter - public static final String RATE_LIMITER = "rate.limiter"; - public static final String RATE_LIMITER_GLOBAL_QPS = "rate.limiter.global.qps"; - public static final String RATE_LIMITER_GLOBAL_IP_QPS = "rate.limiter.global.ip.qps"; - public static final String RATE_LIMITER_GLOBAL_API_QPS = "rate.limiter.global.api.qps"; - public static final String RATE_LIMITER_HTTP = "rate.limiter.http"; - public static final String RATE_LIMITER_RPC = "rate.limiter.rpc"; - public static final String RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN = - "rate.limiter.p2p.syncBlockChain"; - public static final String RATE_LIMITER_P2P_FETCH_INV_DATA = "rate.limiter.p2p.fetchInvData"; - public static final String RATE_LIMITER_P2P_DISCONNECT = "rate.limiter.p2p.disconnect"; - - // metrics - public static final String METRICS_STORAGE_ENABLE = "node.metrics.storageEnable"; - public static final String METRICS_INFLUXDB_IP = "node.metrics.influxdb.ip"; - public static final String METRICS_INFLUXDB_PORT = "node.metrics.influxdb.port"; - public static final String METRICS_INFLUXDB_DATABASE = "node.metrics.influxdb.database"; - public static final String METRICS_REPORT_INTERVAL = - "node.metrics.influxdb.metricsReportInterval"; - public static final String METRICS_PROMETHEUS_ENABLE = "node.metrics.prometheus.enable"; - public static final String METRICS_PROMETHEUS_PORT = "node.metrics.prometheus.port"; - - // seed - public static final String SEED_NODE_IP_LIST = "seed.node.ip.list"; - - // transaction - public static final String TRX_REFERENCE_BLOCK = "trx.reference.block"; - public static final String TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS = - "trx.expiration.timeInMilliseconds"; - - // energy - public static final String ENERGY_LIMIT_BLOCK_NUM = "enery.limit.block.num"; - - // actuator - public static final String ACTUATOR_WHITELIST = "actuator.whitelist"; -} diff --git a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java index 04ba1b306eb..5a9923b16c9 100644 --- a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java +++ b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java @@ -61,15 +61,16 @@ public void run() { public void reload() { logger.debug("Reloading ... "); Config config = Configuration.getByFileName(Args.getConfigFilePath()); + NodeConfig nodeConfig = NodeConfig.fromConfig(config); - updateActiveNodes(config); + updateActiveNodes(nodeConfig); - updateTrustNodes(config); + updateTrustNodes(nodeConfig); } - private void updateActiveNodes(Config config) { + private void updateActiveNodes(NodeConfig nodeConfig) { List newActiveNodes = - Args.getInetSocketAddress(config, ConfigKey.NODE_ACTIVE, true); + Args.filterInetSocketAddress(nodeConfig.getActive(), true); parameter.setActiveNodes(newActiveNodes); List activeNodes = TronNetService.getP2pConfig().getActiveNodes(); activeNodes.clear(); @@ -78,8 +79,11 @@ private void updateActiveNodes(Config config) { TronNetService.getP2pConfig().getActiveNodes().toString()); } - private void updateTrustNodes(Config config) { - List newPassiveNodes = Args.getInetAddress(config, ConfigKey.NODE_PASSIVE); + private void updateTrustNodes(NodeConfig nodeConfig) { + List newPassiveNodes = new java.util.ArrayList<>(); + for (InetSocketAddress sa : Args.filterInetSocketAddress(nodeConfig.getPassive(), false)) { + newPassiveNodes.add(sa.getAddress()); + } parameter.setPassiveNodes(newPassiveNodes); List trustNodes = TronNetService.getP2pConfig().getTrustNodes(); trustNodes.clear(); diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 88d893bcf97..e0d9d456e9a 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -143,14 +143,12 @@ public void testIpFromLibP2p() String configuredExternalIp = parameter.getNodeExternalIp(); Assert.assertEquals("46.168.1.1", configuredExternalIp); - Config config = Configuration.getByFileName(TestConstants.TEST_CONF); - Config config3 = config.withoutPath(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP); - CommonParameter.getInstance().setNodeExternalIp(null); - Method method2 = Args.class.getDeclaredMethod("externalIp", Config.class); + NodeConfig nc = new NodeConfig(); + Method method2 = Args.class.getDeclaredMethod("externalIp", NodeConfig.class); method2.setAccessible(true); - method2.invoke(Args.class, config3); + method2.invoke(Args.class, nc); Assert.assertNotEquals(configuredExternalIp, parameter.getNodeExternalIp()); } @@ -166,7 +164,9 @@ public void testInitService() { Map storage = new HashMap<>(); // avoid the exception for the missing storage storage.put("storage.db.directory", "database"); - Config config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test default value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -193,7 +193,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "true"); storage.put("node.jsonrpc.maxBlockRange", "10"); storage.put("node.jsonrpc.maxSubTopics", "20"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -220,7 +222,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "false"); storage.put("node.jsonrpc.maxBlockRange", "5000"); storage.put("node.jsonrpc.maxSubTopics", "1000"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -247,7 +251,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "true"); storage.put("node.jsonrpc.maxBlockRange", "30"); storage.put("node.jsonrpc.maxSubTopics", "40"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -265,7 +271,9 @@ public void testInitService() { // test set invalid value storage.put("node.jsonrpc.maxBlockRange", "0"); storage.put("node.jsonrpc.maxSubTopics", "0"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // check value Args.applyConfigParams(config); Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -274,7 +282,9 @@ public void testInitService() { // test set invalid value storage.put("node.jsonrpc.maxBlockRange", "-2"); storage.put("node.jsonrpc.maxSubTopics", "-4"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // check value Args.applyConfigParams(config); Assert.assertEquals(-2, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -360,5 +370,50 @@ public void testConfigStorageDefaults() { Args.clearParam(); } + + // =========================================================================== + // Boundary tests for clamps applied in Args.java bridge code (not in + // bean postProcess()). + // + // fetchBlockTimeout is read from NodeConfig but clamped in Args.applyNodeConfig + // to range [100, 1000]. Pin this clamp here so any future refactor that moves + // it (e.g. into NodeConfig.postProcess()) preserves the behavior. + // =========================================================================== + + @Test + public void testFetchBlockTimeoutClampedBelowMin() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "50"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(100, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } + + @Test + public void testFetchBlockTimeoutClampedAboveMax() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "2000"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(1000, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } + + @Test + public void testFetchBlockTimeoutInRangeUnchanged() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "500"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(500, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } } diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index e965ae3fd60..344b6dd946d 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -139,8 +139,9 @@ public void shutdownBlockTimeInitTest() { Map params = new HashMap<>(); params.put("node.shutdown.BlockTime", "0"); params.put("storage.db.directory", "database"); - Config config = ConfigFactory.defaultOverrides().withFallback( - ConfigFactory.parseMap(params)); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(params)) + .withFallback(ConfigFactory.defaultReference()); TronError thrown = assertThrows(TronError.class, () -> Args.applyConfigParams(config)); assertEquals(TronError.ErrCode.AUTO_STOP_PARAMS, thrown.getErrCode()); } diff --git a/framework/src/test/resources/config-localtest.conf b/framework/src/test/resources/config-localtest.conf index f1f40dead76..53a78d3e4c6 100644 --- a/framework/src/test/resources/config-localtest.conf +++ b/framework/src/test/resources/config-localtest.conf @@ -57,7 +57,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node.backup { diff --git a/framework/src/test/resources/config-test-dbbackup.conf b/framework/src/test/resources/config-test-dbbackup.conf index 44dd0164b2d..b660965f3e9 100644 --- a/framework/src/test/resources/config-test-dbbackup.conf +++ b/framework/src/test/resources/config-test-dbbackup.conf @@ -60,7 +60,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node.backup { diff --git a/framework/src/test/resources/config-test-index.conf b/framework/src/test/resources/config-test-index.conf index b41fdfe8505..72e4a04f612 100644 --- a/framework/src/test/resources/config-test-index.conf +++ b/framework/src/test/resources/config-test-index.conf @@ -54,7 +54,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node { From 636667a24816a7005237639d997e276efb3253ae Mon Sep 17 00:00:00 2001 From: barbatos2011 <162298485+barbatos2011@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:54:09 +0800 Subject: [PATCH 26/43] feat(plugins): migrate keystore CLI from FullNode to Toolkit (#6637) --- .../java/org/tron/keystore/Credentials.java | 0 .../main/java/org/tron/keystore/Wallet.java | 81 +- .../java/org/tron/keystore/WalletFile.java | 2 +- .../java/org/tron/keystore/WalletUtils.java | 267 ++++++ .../core/config/args/WitnessInitializer.java | 17 +- .../java/org/tron/keystore/WalletUtils.java | 166 ---- .../org/tron/program/KeystoreFactory.java | 35 +- .../args/WitnessInitializerKeystoreTest.java | 207 +++++ .../config/args/WitnessInitializerTest.java | 3 +- .../org/tron/keystore/CredentialsTest.java | 1 - .../java/org/tron/keystore/CrossImplTest.java | 165 ++++ .../keystore/WalletAddressValidationTest.java | 93 ++ .../org/tron/keystore/WalletFilePojoTest.java | 389 +++++++++ .../org/tron/keystore/WalletPropertyTest.java | 77 ++ .../WalletUtilsInputPasswordTest.java | 167 ++++ .../tron/keystore/WalletUtilsWriteTest.java | 204 +++++ .../KeystoreFactoryDeprecationTest.java | 147 ++++ .../java/org/tron/program/SupplementTest.java | 7 - plugins/README.md | 75 ++ plugins/build.gradle | 6 + .../common/org/tron/plugins/Keystore.java | 19 + .../org/tron/plugins/KeystoreCliUtils.java | 304 +++++++ .../org/tron/plugins/KeystoreImport.java | 188 ++++ .../common/org/tron/plugins/KeystoreList.java | 110 +++ .../common/org/tron/plugins/KeystoreNew.java | 77 ++ .../org/tron/plugins/KeystoreUpdate.java | 245 ++++++ .../java/common/org/tron/plugins/Toolkit.java | 2 +- .../tron/plugins/KeystoreCliUtilsTest.java | 348 ++++++++ .../org/tron/plugins/KeystoreImportTest.java | 536 ++++++++++++ .../org/tron/plugins/KeystoreListTest.java | 282 ++++++ .../org/tron/plugins/KeystoreNewTest.java | 307 +++++++ .../org/tron/plugins/KeystoreUpdateTest.java | 822 ++++++++++++++++++ 32 files changed, 5145 insertions(+), 204 deletions(-) rename {framework => crypto}/src/main/java/org/tron/keystore/Credentials.java (100%) rename {framework => crypto}/src/main/java/org/tron/keystore/Wallet.java (74%) rename {framework => crypto}/src/main/java/org/tron/keystore/WalletFile.java (99%) create mode 100644 crypto/src/main/java/org/tron/keystore/WalletUtils.java delete mode 100644 framework/src/main/java/org/tron/keystore/WalletUtils.java create mode 100644 framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java create mode 100644 framework/src/test/java/org/tron/keystore/CrossImplTest.java create mode 100644 framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java create mode 100644 framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java create mode 100644 framework/src/test/java/org/tron/keystore/WalletPropertyTest.java create mode 100644 framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java create mode 100644 framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java create mode 100644 framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/Keystore.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/KeystoreList.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java create mode 100644 plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java create mode 100644 plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java create mode 100644 plugins/src/test/java/org/tron/plugins/KeystoreListTest.java create mode 100644 plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java create mode 100644 plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java diff --git a/framework/src/main/java/org/tron/keystore/Credentials.java b/crypto/src/main/java/org/tron/keystore/Credentials.java similarity index 100% rename from framework/src/main/java/org/tron/keystore/Credentials.java rename to crypto/src/main/java/org/tron/keystore/Credentials.java diff --git a/framework/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java similarity index 74% rename from framework/src/main/java/org/tron/keystore/Wallet.java rename to crypto/src/main/java/org/tron/keystore/Wallet.java index d38b1c74984..d63525b1e4d 100644 --- a/framework/src/main/java/org/tron/keystore/Wallet.java +++ b/crypto/src/main/java/org/tron/keystore/Wallet.java @@ -23,7 +23,6 @@ import org.tron.common.crypto.SignUtils; import org.tron.common.utils.ByteArray; import org.tron.common.utils.StringUtil; -import org.tron.core.config.args.Args; import org.tron.core.exception.CipherException; /** @@ -48,7 +47,12 @@ */ public class Wallet { - protected static final String AES_128_CTR = "pbkdf2"; + // KDF identifiers used in the Web3 Secret Storage "kdf" field. + // The old name "AES_128_CTR" was misleading — the value is the PBKDF2 KDF + // identifier, not the cipher (CIPHER below). The inner class name + // `WalletFile.Aes128CtrKdfParams` is kept for wire-format/Jackson-subtype + // backward compatibility even though it also reflects the same history. + protected static final String PBKDF2 = "pbkdf2"; protected static final String SCRYPT = "scrypt"; private static final int N_LIGHT = 1 << 12; private static final int P_LIGHT = 6; @@ -168,8 +172,8 @@ private static byte[] generateMac(byte[] derivedKey, byte[] cipherText) { return Hash.sha3(result); } - public static SignInterface decrypt(String password, WalletFile walletFile) - throws CipherException { + public static SignInterface decrypt(String password, WalletFile walletFile, + boolean ecKey) throws CipherException { validate(walletFile); @@ -205,32 +209,79 @@ public static SignInterface decrypt(String password, WalletFile walletFile) byte[] derivedMac = generateMac(derivedKey, cipherText); - if (!Arrays.equals(derivedMac, mac)) { + if (!java.security.MessageDigest.isEqual(derivedMac, mac)) { throw new CipherException("Invalid password provided"); } byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16); byte[] privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText); - return SignUtils.fromPrivate(privateKey, Args.getInstance().isECKeyCryptoEngine()); - } + SignInterface keyPair = SignUtils.fromPrivate(privateKey, ecKey); + + // Enforce address consistency: if the keystore declares an address, it MUST match + // the address derived from the decrypted private key. Prevents address spoofing + // where a crafted keystore displays one address but encrypts a different key. + String declared = walletFile.getAddress(); + if (declared != null && !declared.isEmpty()) { + String derived = StringUtil.encode58Check(keyPair.getAddress()); + if (!declared.equals(derived)) { + throw new CipherException( + "Keystore address mismatch: file declares " + declared + + " but private key derives " + derived); + } + } - static void validate(WalletFile walletFile) throws CipherException { - WalletFile.Crypto crypto = walletFile.getCrypto(); + return keyPair; + } + /** + * Returns a description of the first schema violation found in + * {@code walletFile}, or {@code null} if the file matches the supported + * V3 keystore shape (current version, known cipher, known KDF). + * + *

Shared by {@link #validate(WalletFile)} (which throws the message) + * and {@link #isValidKeystoreFile(WalletFile)} (which returns boolean + * for discovery-style filtering). + */ + private static String validationError(WalletFile walletFile) { if (walletFile.getVersion() != CURRENT_VERSION) { - throw new CipherException("Wallet version is not supported"); + return "Wallet version is not supported"; } - - if (!crypto.getCipher().equals(CIPHER)) { - throw new CipherException("Wallet cipher is not supported"); + WalletFile.Crypto crypto = walletFile.getCrypto(); + if (crypto == null) { + return "Missing crypto section"; + } + String cipher = crypto.getCipher(); + if (cipher == null || !cipher.equals(CIPHER)) { + return "Wallet cipher is not supported"; } + String kdf = crypto.getKdf(); + if (kdf == null || (!kdf.equals(PBKDF2) && !kdf.equals(SCRYPT))) { + return "KDF type is not supported"; + } + return null; + } - if (!crypto.getKdf().equals(AES_128_CTR) && !crypto.getKdf().equals(SCRYPT)) { - throw new CipherException("KDF type is not supported"); + static void validate(WalletFile walletFile) throws CipherException { + String error = validationError(walletFile); + if (error != null) { + throw new CipherException(error); } } + /** + * Returns {@code true} iff {@code walletFile} has the shape of a + * decryptable V3 keystore: non-null address, supported version, non-null + * crypto section with a supported cipher and KDF. Intended for + * discovery-style filtering (e.g. listing or duplicate detection) where + * we want to skip JSON stubs that would later fail {@link #validate}. + */ + public static boolean isValidKeystoreFile(WalletFile walletFile) { + return walletFile != null + && walletFile.getAddress() != null + && validationError(walletFile) == null; + } + public static byte[] generateRandomBytes(int size) { byte[] bytes = new byte[size]; new SecureRandom().nextBytes(bytes); diff --git a/framework/src/main/java/org/tron/keystore/WalletFile.java b/crypto/src/main/java/org/tron/keystore/WalletFile.java similarity index 99% rename from framework/src/main/java/org/tron/keystore/WalletFile.java rename to crypto/src/main/java/org/tron/keystore/WalletFile.java index 1f5135fefd3..97e538d1a8a 100644 --- a/framework/src/main/java/org/tron/keystore/WalletFile.java +++ b/crypto/src/main/java/org/tron/keystore/WalletFile.java @@ -165,7 +165,7 @@ public KdfParams getKdfparams() { include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "kdf") @JsonSubTypes({ - @JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.AES_128_CTR), + @JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.PBKDF2), @JsonSubTypes.Type(value = ScryptKdfParams.class, name = Wallet.SCRYPT) }) // To support my Ether Wallet keys uncomment this annotation & comment out the above diff --git a/crypto/src/main/java/org/tron/keystore/WalletUtils.java b/crypto/src/main/java/org/tron/keystore/WalletUtils.java new file mode 100644 index 00000000000..2ce100823d9 --- /dev/null +++ b/crypto/src/main/java/org/tron/keystore/WalletUtils.java @@ -0,0 +1,267 @@ +package org.tron.keystore; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Scanner; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.tron.common.crypto.SignInterface; +import org.tron.core.exception.CipherException; + +/** + * Utility functions for working with Wallet files. + */ +@Slf4j(topic = "keystore") +public class WalletUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final Set OWNER_ONLY = + Collections.unmodifiableSet(EnumSet.of( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + + static { + objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public static String generateWalletFile( + String password, SignInterface ecKeyPair, File destinationDirectory, boolean useFullScrypt) + throws CipherException, IOException { + + WalletFile walletFile; + if (useFullScrypt) { + walletFile = Wallet.createStandard(password, ecKeyPair); + } else { + walletFile = Wallet.createLight(password, ecKeyPair); + } + + String fileName = getWalletFileName(walletFile); + File destination = new File(destinationDirectory, fileName); + writeWalletFile(walletFile, destination); + + return fileName; + } + + /** + * Write a WalletFile to the given destination path with owner-only (0600) + * permissions, using a temp file + atomic rename. + * + *

On POSIX filesystems, the temp file is created atomically with 0600 + * permissions via {@link Files#createTempFile(Path, String, String, + * java.nio.file.attribute.FileAttribute[])}, avoiding any window where the + * file is world-readable. + * + *

On non-POSIX filesystems (e.g. Windows) the fallback uses + * {@link File#setReadable(boolean, boolean)} / + * {@link File#setWritable(boolean, boolean)} which is best-effort — these + * methods manipulate only DOS-style attributes on Windows and may not update + * file ACLs. The sensitive keystore JSON is written only after the narrowing + * calls, so no confidential data is exposed during the window, but callers + * on Windows should not infer strict owner-only ACL enforcement from this. + * + * @param walletFile the keystore to serialize + * @param destination the final target file (existing file will be replaced) + */ + public static void writeWalletFile(WalletFile walletFile, File destination) + throws IOException { + Path dir = destination.getAbsoluteFile().getParentFile().toPath(); + Files.createDirectories(dir); + + Path tmp; + try { + tmp = Files.createTempFile(dir, "keystore-", ".tmp", + PosixFilePermissions.asFileAttribute(OWNER_ONLY)); + } catch (UnsupportedOperationException e) { + // Windows / non-POSIX fallback — best-effort narrowing only (see JavaDoc) + tmp = Files.createTempFile(dir, "keystore-", ".tmp"); + File tf = tmp.toFile(); + tf.setReadable(false, false); + tf.setReadable(true, true); + tf.setWritable(false, false); + tf.setWritable(true, true); + } + + try { + objectMapper.writeValue(tmp.toFile(), walletFile); + try { + Files.move(tmp, destination.toPath(), + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (Exception e) { + try { + Files.deleteIfExists(tmp); + } catch (IOException suppress) { + e.addSuppressed(suppress); + } + throw e; + } + } + + public static Credentials loadCredentials(String password, File source, boolean ecKey) + throws IOException, CipherException { + warnIfSymbolicLink(source); + WalletFile walletFile = objectMapper.readValue(source, WalletFile.class); + return Credentials.create(Wallet.decrypt(password, walletFile, ecKey)); + } + + /** + * Emit a warning if {@code source} is a symbolic link. The keystore is still + * read (following the symlink), preserving compatibility with legitimate + * deployments that use symlinks to organize keystore files (e.g. + * {@code /opt/tron/keystore/witness.json} -> {@code /mnt/encrypted/...}, + * container volume-mount paths). The warning gives operators a chance to + * notice if a path they did not expect to be a symlink has become one — for + * example if an attacker with config-injection ability has redirected the + * SR startup keystore. This mirrors how Ethereum consensus clients (e.g. + * Lighthouse) handle a configured {@code voting_keystore_path}. + */ + private static void warnIfSymbolicLink(File source) { + try { + BasicFileAttributes attrs = Files.readAttributes(source.toPath(), + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attrs.isSymbolicLink()) { + logger.warn("Keystore file is a symbolic link: {} — proceeding, " + + "but verify the symlink target points where you expect.", + source.getPath()); + } + } catch (IOException ignored) { + // If we can't stat, let the subsequent readValue surface the real error. + } + } + + public static String getWalletFileName(WalletFile walletFile) { + DateTimeFormatter format = DateTimeFormatter.ofPattern( + "'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'"); + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + + return now.format(format) + walletFile.getAddress() + ".json"; + } + + /** + * Strip trailing line terminators ({@code \n}/{@code \r}) and a leading + * UTF-8 BOM ({@code \uFEFF}) from a line of input. Unlike + * {@link String#trim()} this preserves internal whitespace, so passwords + * containing spaces (e.g. passphrases) survive intact. + * + *

Intended as the canonical helper for normalizing raw user-provided + * password/line input across both CLI console and file-driven paths. + * Returns {@code null} if the input is {@code null}. + */ + public static String stripPasswordLine(String s) { + if (s == null) { + return null; + } + if (s.length() > 0 && s.charAt(0) == '\uFEFF') { + s = s.substring(1); + } + int end = s.length(); + while (end > 0) { + char c = s.charAt(end - 1); + if (c == '\n' || c == '\r') { + end--; + } else { + break; + } + } + return s.substring(0, end); + } + + public static boolean passwordValid(String password) { + if (StringUtils.isEmpty(password)) { + return false; + } + if (password.length() < 6) { + return false; + } + //Other rule; + return true; + } + + /** + * Lazily-initialized Scanner shared across successive + * {@link #inputPassword()} calls on the non-TTY path so that + * {@link #inputPassword2Twice()} can read two lines in sequence + * without losing data. Each call to {@code new Scanner(System.in)} + * internally buffers bytes from the underlying {@link BufferedReader}; + * constructing a second Scanner after the first has been discarded + * drops any buffered bytes the first pulled from stdin, causing + * {@code NoSuchElementException}. + */ + private static Scanner sharedStdinScanner; + + /** + * Visible for testing: reset the cached Scanner so subsequent calls + * see a freshly rebound {@link System#in}. + */ + static synchronized void resetSharedStdinScanner() { + sharedStdinScanner = null; + } + + private static synchronized Scanner getSharedStdinScanner() { + if (sharedStdinScanner == null) { + sharedStdinScanner = new Scanner(System.in); + } + return sharedStdinScanner; + } + + public static String inputPassword() { + String password; + Console cons = System.console(); + Scanner in = cons == null ? getSharedStdinScanner() : null; + while (true) { + if (cons != null) { + char[] pwd = cons.readPassword("password: "); + password = String.valueOf(pwd); + } else { + // Preserve the full password including embedded whitespace. + // The previous implementation applied trim() + split("\\s+")[0] + // which silently truncated passwords like "correct horse battery + // staple" to "correct" when piped via stdin (e.g. echo ... | java). + // stripPasswordLine only removes the UTF-8 BOM and trailing line + // terminators — internal whitespace is part of the password. + password = stripPasswordLine(in.nextLine()); + } + if (passwordValid(password)) { + return password; + } + System.out.println("Invalid password, please input again."); + } + } + + public static String inputPassword2Twice() { + String password0; + while (true) { + System.out.println("Please input password."); + password0 = inputPassword(); + System.out.println("Please input password again."); + String password1 = inputPassword(); + if (password0.equals(password1)) { + break; + } + System.out.println("Two passwords do not match, please input again."); + } + return password0; + } +} diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java index 30711eb6190..c2ce2ba0046 100644 --- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java +++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java @@ -79,12 +79,27 @@ public static LocalWitnesses initFromKeystore( List privateKeys = new ArrayList<>(); try { - Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName)); + Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName), + Args.getInstance().isECKeyCryptoEngine()); SignInterface sign = credentials.getSignInterface(); String prikey = ByteArray.toHexString(sign.getPrivateKey()); privateKeys.add(prikey); } catch (IOException | CipherException e) { logger.error("Witness node start failed!"); + // Legacy-truncation hint: if this keystore was created with + // `FullNode.jar --keystore-factory` in non-TTY mode (e.g. + // `echo PASS | java ...`), the legacy code encrypted with only + // the first whitespace-separated word of the password. Emit the + // tip only when the entered password has internal whitespace — + // otherwise truncation cannot be the cause. + if (e instanceof CipherException && pwd != null && pwd.matches(".*\\s.*")) { + logger.error( + "Tip: keystores created via `FullNode.jar --keystore-factory` in " + + "non-TTY mode were encrypted with only the first " + + "whitespace-separated word of the password. Try restarting " + + "with only that first word as `-p`, then reset the password " + + "via `java -jar Toolkit.jar keystore update`."); + } throw new TronError(e, TronError.ErrCode.WITNESS_KEYSTORE_LOAD); } diff --git a/framework/src/main/java/org/tron/keystore/WalletUtils.java b/framework/src/main/java/org/tron/keystore/WalletUtils.java deleted file mode 100644 index 8bcc68cbab0..00000000000 --- a/framework/src/main/java/org/tron/keystore/WalletUtils.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.tron.keystore; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.Console; -import java.io.File; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Scanner; -import org.apache.commons.lang3.StringUtils; -import org.tron.common.crypto.SignInterface; -import org.tron.common.crypto.SignUtils; -import org.tron.common.utils.Utils; -import org.tron.core.config.args.Args; -import org.tron.core.exception.CipherException; - -/** - * Utility functions for working with Wallet files. - */ -public class WalletUtils { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - static { - objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - public static String generateFullNewWalletFile(String password, File destinationDirectory) - throws NoSuchAlgorithmException, NoSuchProviderException, - InvalidAlgorithmParameterException, CipherException, IOException { - - return generateNewWalletFile(password, destinationDirectory, true); - } - - public static String generateLightNewWalletFile(String password, File destinationDirectory) - throws NoSuchAlgorithmException, NoSuchProviderException, - InvalidAlgorithmParameterException, CipherException, IOException { - - return generateNewWalletFile(password, destinationDirectory, false); - } - - public static String generateNewWalletFile( - String password, File destinationDirectory, boolean useFullScrypt) - throws CipherException, IOException, InvalidAlgorithmParameterException, - NoSuchAlgorithmException, NoSuchProviderException { - - SignInterface ecKeyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), - Args.getInstance().isECKeyCryptoEngine()); - return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt); - } - - public static String generateWalletFile( - String password, SignInterface ecKeyPair, File destinationDirectory, boolean useFullScrypt) - throws CipherException, IOException { - - WalletFile walletFile; - if (useFullScrypt) { - walletFile = Wallet.createStandard(password, ecKeyPair); - } else { - walletFile = Wallet.createLight(password, ecKeyPair); - } - - String fileName = getWalletFileName(walletFile); - File destination = new File(destinationDirectory, fileName); - - objectMapper.writeValue(destination, walletFile); - - return fileName; - } - - public static Credentials loadCredentials(String password, File source) - throws IOException, CipherException { - WalletFile walletFile = objectMapper.readValue(source, WalletFile.class); - return Credentials.create(Wallet.decrypt(password, walletFile)); - } - - private static String getWalletFileName(WalletFile walletFile) { - DateTimeFormatter format = DateTimeFormatter.ofPattern( - "'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'"); - ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); - - return now.format(format) + walletFile.getAddress() + ".json"; - } - - public static String getDefaultKeyDirectory() { - return getDefaultKeyDirectory(System.getProperty("os.name")); - } - - static String getDefaultKeyDirectory(String osName1) { - String osName = osName1.toLowerCase(); - - if (osName.startsWith("mac")) { - return String.format( - "%s%sLibrary%sEthereum", System.getProperty("user.home"), File.separator, - File.separator); - } else if (osName.startsWith("win")) { - return String.format("%s%sEthereum", System.getenv("APPDATA"), File.separator); - } else { - return String.format("%s%s.ethereum", System.getProperty("user.home"), File.separator); - } - } - - public static String getTestnetKeyDirectory() { - return String.format( - "%s%stestnet%skeystore", getDefaultKeyDirectory(), File.separator, File.separator); - } - - public static String getMainnetKeyDirectory() { - return String.format("%s%skeystore", getDefaultKeyDirectory(), File.separator); - } - - public static boolean passwordValid(String password) { - if (StringUtils.isEmpty(password)) { - return false; - } - if (password.length() < 6) { - return false; - } - //Other rule; - return true; - } - - public static String inputPassword() { - Scanner in = null; - String password; - Console cons = System.console(); - if (cons == null) { - in = new Scanner(System.in); - } - while (true) { - if (cons != null) { - char[] pwd = cons.readPassword("password: "); - password = String.valueOf(pwd); - } else { - String input = in.nextLine().trim(); - password = input.split("\\s+")[0]; - } - if (passwordValid(password)) { - return password; - } - System.out.println("Invalid password, please input again."); - } - } - - public static String inputPassword2Twice() { - String password0; - while (true) { - System.out.println("Please input password."); - password0 = inputPassword(); - System.out.println("Please input password again."); - String password1 = inputPassword(); - if (password0.equals(password1)) { - break; - } - System.out.println("Two passwords do not match, please input again."); - } - return password0; - } -} diff --git a/framework/src/main/java/org/tron/program/KeystoreFactory.java b/framework/src/main/java/org/tron/program/KeystoreFactory.java index 8199d7e9076..a88cdca904a 100755 --- a/framework/src/main/java/org/tron/program/KeystoreFactory.java +++ b/framework/src/main/java/org/tron/program/KeystoreFactory.java @@ -15,11 +15,20 @@ import org.tron.keystore.WalletUtils; @Slf4j(topic = "app") +@Deprecated public class KeystoreFactory { private static final String FilePath = "Wallet"; public static void start() { + System.err.println("WARNING: --keystore-factory is deprecated and will be removed " + + "in a future release."); + System.err.println("Please use: java -jar Toolkit.jar keystore "); + System.err.println(" keystore new - Generate a new keystore"); + System.err.println(" keystore import - Import a private key"); + System.err.println(" keystore list - List keystores"); + System.err.println(" keystore update - Change password"); + System.err.println(); KeystoreFactory cli = new KeystoreFactory(); cli.run(); } @@ -57,15 +66,16 @@ private void fileCheck(File file) throws IOException { private void genKeystore() throws CipherException, IOException { + boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine(); String password = WalletUtils.inputPassword2Twice(); - SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random, - CommonParameter.getInstance().isECKeyCryptoEngine()); + SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random, ecKey); File file = new File(FilePath); fileCheck(file); String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true); System.out.println("Gen a keystore its name " + fileName); - Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName)); + Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName), + ecKey); System.out.println("Your address is " + credentials.getAddress()); } @@ -84,22 +94,25 @@ private void importPrivateKey() throws CipherException, IOException { String password = WalletUtils.inputPassword2Twice(); - SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey), - CommonParameter.getInstance().isECKeyCryptoEngine()); + boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine(); + SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey), ecKey); File file = new File(FilePath); fileCheck(file); String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true); System.out.println("Gen a keystore its name " + fileName); - Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName)); + Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName), + ecKey); System.out.println("Your address is " + credentials.getAddress()); } private void help() { - System.out.println("You can enter the following command: "); - System.out.println("GenKeystore"); - System.out.println("ImportPrivateKey"); - System.out.println("Exit or Quit"); - System.out.println("Input any one of them, you will get more tips."); + System.out.println("NOTE: --keystore-factory is deprecated. Use Toolkit.jar instead:"); + System.out.println(" java -jar Toolkit.jar keystore new|import|list|update"); + System.out.println(); + System.out.println("Legacy commands (will be removed):"); + System.out.println(" GenKeystore"); + System.out.println(" ImportPrivateKey"); + System.out.println(" Exit or Quit"); } private void run() { diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java new file mode 100644 index 00000000000..80d8287682b --- /dev/null +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java @@ -0,0 +1,207 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.security.SecureRandom; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.LocalWitnesses; +import org.tron.core.exception.TronError; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; + +/** + * Backward compatibility: verifies that keystore files generated by + * the new Toolkit code path can be loaded by WitnessInitializer + * (used by FullNode at startup via localwitnesskeystore config). + */ +public class WitnessInitializerKeystoreTest { + + @ClassRule + public static final TemporaryFolder tempFolder = new TemporaryFolder(); + + // WitnessInitializer prepends user.dir to the filename, so we must + // create the keystore dir relative to user.dir. Use unique name to + // avoid collisions with parallel test runs. + private static final String DIR_NAME = + ".test-keystore-" + System.currentTimeMillis(); + + private static String keystoreFileName; + private static String expectedPrivateKey; + private static final String PASSWORD = "backcompat123"; + + @BeforeClass + public static void setUp() throws Exception { + Args.setParam(new String[]{"-d", tempFolder.newFolder().toString()}, + "config-test.conf"); + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + expectedPrivateKey = ByteArray.toHexString(keyPair.getPrivateKey()); + + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + dir.mkdirs(); + String generatedName = + WalletUtils.generateWalletFile(PASSWORD, keyPair, dir, true); + keystoreFileName = DIR_NAME + "/" + generatedName; + } + + @AfterClass + public static void tearDown() { + Args.clearParam(); + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + if (dir.exists()) { + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + f.delete(); + } + } + dir.delete(); + } + } + + @Test + public void testNewKeystoreLoadableByWitnessInitializer() { + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + + LocalWitnesses result = WitnessInitializer.initFromKeystore( + keystores, PASSWORD, null); + + assertNotNull("WitnessInitializer should load new keystore", result); + assertFalse("Should have at least one private key", + result.getPrivateKeys().isEmpty()); + assertEquals("Private key must match original", + expectedPrivateKey, result.getPrivateKeys().get(0)); + } + + @Test + public void testLegacyTruncationTipFiresOnWhitespacePassword() { + // The SR startup path should mirror the Toolkit's behavior: when the + // supplied password contains whitespace and decryption fails, emit the + // legacy-truncation hint pointing operators at the FullNode keystore- + // factory bug. The Toolkit covers this in + // KeystoreUpdateTest#testUpdateLegacyTipFiresWhenPasswordHasWhitespace. + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + ListAppender appender = attachAppender(); + try { + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore( + keystores, "wrong pass with spaces", null)); + assertEquals(TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + String logs = renderLogs(appender); + assertTrue("Legacy-truncation tip must fire for whitespace password," + + " got: " + logs, + logs.contains("first whitespace-separated word")); + } finally { + detachAppender(appender); + } + } + + @Test + public void testLegacyTruncationTipSuppressedOnNoWhitespacePassword() { + // For the common "wrong password" case (no whitespace), the legacy tip + // would be misleading noise — it must be suppressed while still surfacing + // the underlying load failure. + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + ListAppender appender = attachAppender(); + try { + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore( + keystores, "wrongnospaces", null)); + assertEquals(TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + String logs = renderLogs(appender); + assertTrue("Witness load failure must still be logged, got: " + logs, + logs.contains("Witness node start failed")); + assertFalse("Legacy-truncation tip must NOT fire for whitespace-free" + + " password, got: " + logs, + logs.contains("first whitespace-separated word")); + } finally { + detachAppender(appender); + } + } + + @Test + public void testTamperedKeystoreRejectedAtSrLoading() throws Exception { + // Address-spoofing defense: a keystore whose declared `address` field does + // not match the address derived from the decrypted private key must be + // rejected at decryption time. Without this check, an attacker who could + // place a file in the SR's keystore dir could trick a witness into signing + // with a different key while displaying a familiar address. The check + // lives in Wallet.decrypt; this test verifies it propagates correctly + // through the WitnessInitializer path. + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String pwd = "tamperpwd123"; + String generatedName = WalletUtils.generateWalletFile(pwd, keyPair, dir, true); + File keystoreFile = new File(dir, generatedName); + try { + // Tamper: rewrite the address field to a different value than what the + // encrypted private key actually derives to. + ObjectMapper mapper = new ObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + WalletFile wf = mapper.readValue(keystoreFile, WalletFile.class); + String spoofedAddress = "TSpoofedSrAddressXXXXXXXXXXXXXXXXXXX"; + wf.setAddress(spoofedAddress); + mapper.writeValue(keystoreFile, wf); + + java.util.List keystores = + java.util.Collections.singletonList(DIR_NAME + "/" + generatedName); + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore(keystores, pwd, null)); + assertEquals("Should be a witness keystore load failure", + TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + Throwable cause = err.getCause(); + assertNotNull("TronError must wrap the underlying CipherException", cause); + assertNotNull("Cause message must not be null", cause.getMessage()); + assertTrue("Cause must mention address mismatch, got: " + cause.getMessage(), + cause.getMessage().contains("address mismatch")); + } finally { + keystoreFile.delete(); + } + } + + private static ListAppender attachAppender() { + ListAppender appender = new ListAppender<>(); + appender.start(); + Logger logger = (Logger) LoggerFactory.getLogger(WitnessInitializer.class); + logger.addAppender(appender); + return appender; + } + + private static void detachAppender(ListAppender appender) { + Logger logger = (Logger) LoggerFactory.getLogger(WitnessInitializer.class); + logger.detachAppender(appender); + appender.stop(); + } + + private static String renderLogs(ListAppender appender) { + StringBuilder sb = new StringBuilder(); + for (ILoggingEvent event : appender.list) { + sb.append(event.getFormattedMessage()).append('\n'); + } + return sb.toString(); + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java index 3ecef5b10c9..e0aa2606473 100644 --- a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -106,7 +107,7 @@ public void testInitFromKeystore() { byte[] keyBytes = Hex.decode(privateKey); when(signInterface.getPrivateKey()).thenReturn(keyBytes); mockedWallet.when(() -> WalletUtils.loadCredentials( - anyString(), any(File.class))).thenReturn(credentials); + anyString(), any(File.class), anyBoolean())).thenReturn(credentials); mockedByteArray.when(() -> ByteArray.toHexString(any())) .thenReturn(privateKey); mockedByteArray.when(() -> ByteArray.fromHexString(anyString())) diff --git a/framework/src/test/java/org/tron/keystore/CredentialsTest.java b/framework/src/test/java/org/tron/keystore/CredentialsTest.java index 8aabd887bb0..df1b4440e08 100644 --- a/framework/src/test/java/org/tron/keystore/CredentialsTest.java +++ b/framework/src/test/java/org/tron/keystore/CredentialsTest.java @@ -77,5 +77,4 @@ public void testEqualsWithAddressAndCryptoEngine() { Assert.assertNotEquals(credential, sameAddressDifferentEngineCredential); Assert.assertFalse(credential.equals(differentCredential)); } - } diff --git a/framework/src/test/java/org/tron/keystore/CrossImplTest.java b/framework/src/test/java/org/tron/keystore/CrossImplTest.java new file mode 100644 index 00000000000..6b00c57c1f9 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/CrossImplTest.java @@ -0,0 +1,165 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; + +/** + * Format compatibility tests. + * + *

All tests generate keystores dynamically at test time — no static + * fixtures or secrets stored in the repository. Verifies that keystore + * files can survive a full roundtrip: generate keypair, encrypt, serialize + * to JSON file, deserialize, decrypt, compare private key and address. + */ +public class CrossImplTest { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + // --- Ethereum standard test vectors (from Web3 Secret Storage spec, inline) --- + // Source: web3j WalletTest.java — password and private key are public test data. + + private static final String ETH_PASSWORD = "Insecure Pa55w0rd"; + private static final String ETH_PRIVATE_KEY = + "a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6"; + + private static final String ETH_PBKDF2_KEYSTORE = "{" + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"," + + "\"cipherparams\":{\"iv\":\"02ebc768684e5576900376114625ee6f\"}," + + "\"ciphertext\":\"7ad5c9dd2c95f34a92ebb86740b92103a5d1cc4c2eabf3b9a59e1f83f3181216\"," + + "\"kdf\":\"pbkdf2\"," + + "\"kdfparams\":{\"c\":262144,\"dklen\":32,\"prf\":\"hmac-sha256\"," + + "\"salt\":\"0e4cf3893b25bb81efaae565728b5b7cde6a84e224cbf9aed3d69a31c981b702\"}," + + "\"mac\":\"2b29e4641ec17f4dc8b86fc8592090b50109b372529c30b001d4d96249edaf62\"}," + + "\"id\":\"af0451b4-6020-4ef0-91ec-794a5a965b01\",\"version\":3}"; + + private static final String ETH_SCRYPT_KEYSTORE = "{" + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"," + + "\"cipherparams\":{\"iv\":\"3021e1ef4774dfc5b08307f3a4c8df00\"}," + + "\"ciphertext\":\"4dd29ba18478b98cf07a8a44167acdf7e04de59777c4b9c139e3d3fa5cb0b931\"," + + "\"kdf\":\"scrypt\"," + + "\"kdfparams\":{\"dklen\":32,\"n\":262144,\"r\":8,\"p\":1," + + "\"salt\":\"4f9f68c71989eb3887cd947c80b9555fce528f210199d35c35279beb8c2da5ca\"}," + + "\"mac\":\"7e8f2192767af9be18e7a373c1986d9190fcaa43ad689bbb01a62dbde159338d\"}," + + "\"id\":\"7654525c-17e0-4df5-94b5-c7fde752c9d2\",\"version\":3}"; + + @Test + public void testDecryptEthPbkdf2Keystore() throws Exception { + WalletFile walletFile = MAPPER.readValue(ETH_PBKDF2_KEYSTORE, WalletFile.class); + SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true); + assertEquals("Private key must match Ethereum test vector", + ETH_PRIVATE_KEY, + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptEthScryptKeystore() throws Exception { + WalletFile walletFile = MAPPER.readValue(ETH_SCRYPT_KEYSTORE, WalletFile.class); + SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true); + assertEquals("Private key must match Ethereum test vector", + ETH_PRIVATE_KEY, + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + // --- Dynamic format compatibility (no static secrets) --- + + @Test + public void testKeystoreFormatCompatibility() throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String password = "dynamicTest123"; + + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // Verify Web3 Secret Storage structure + assertEquals("version must be 3", 3, walletFile.getVersion()); + assertNotNull("must have address", walletFile.getAddress()); + assertNotNull("must have crypto", walletFile.getCrypto()); + assertEquals("cipher must be aes-128-ctr", + "aes-128-ctr", walletFile.getCrypto().getCipher()); + assertTrue("kdf must be scrypt or pbkdf2", + "scrypt".equals(walletFile.getCrypto().getKdf()) + || "pbkdf2".equals(walletFile.getCrypto().getKdf())); + + // Write to file, read back — simulates cross-process interop + File tempFile = new File(tempFolder.getRoot(), "compat-test.json"); + MAPPER.writeValue(tempFile, walletFile); + WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class); + + SignInterface recovered = Wallet.decrypt(password, loaded, true); + assertArrayEquals("Key must survive file roundtrip", + originalKey, recovered.getPrivateKey()); + + // Verify TRON address format + byte[] tronAddr = recovered.getAddress(); + assertEquals("TRON address must be 21 bytes", 21, tronAddr.length); + assertEquals("First byte must be TRON prefix", 0x41, tronAddr[0] & 0xFF); + } + + @Test + public void testLightScryptFormatCompatibility() throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String password = "lightCompat456"; + + WalletFile walletFile = Wallet.createLight(password, keyPair); + File tempFile = new File(tempFolder.getRoot(), "light-compat.json"); + MAPPER.writeValue(tempFile, walletFile); + WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class); + + SignInterface recovered = Wallet.decrypt(password, loaded, true); + assertArrayEquals("Key must survive light scrypt file roundtrip", + originalKey, recovered.getPrivateKey()); + } + + @Test + public void testKeystoreAddressConsistency() throws Exception { + String password = "addresscheck"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + Credentials original = Credentials.create(keyPair); + + WalletFile walletFile = Wallet.createLight(password, keyPair); + assertEquals("WalletFile address must match credentials address", + original.getAddress(), walletFile.getAddress()); + + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + Credentials recoveredCreds = Credentials.create(recovered); + assertEquals("Recovered address must match original", + original.getAddress(), recoveredCreds.getAddress()); + } + + @Test + public void testLoadCredentialsIntegration() throws Exception { + String password = "integration789"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String originalAddress = Credentials.create(keyPair).getAddress(); + + File tempDir = tempFolder.newFolder("wallet-integration"); + String fileName = WalletUtils.generateWalletFile(password, keyPair, tempDir, false); + assertNotNull(fileName); + + File keystoreFile = new File(tempDir, fileName); + Credentials loaded = WalletUtils.loadCredentials(password, keystoreFile, true); + + assertEquals("Address must survive full WalletUtils roundtrip", + originalAddress, loaded.getAddress()); + assertArrayEquals("Key must survive full WalletUtils roundtrip", + originalKey, loaded.getSignInterface().getPrivateKey()); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java new file mode 100644 index 00000000000..82008988b6e --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java @@ -0,0 +1,93 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; + +/** + * Verifies that Wallet.decrypt rejects keystores whose declared address + * does not match the address derived from the decrypted private key, + * preventing address-spoofing attacks. + */ +public class WalletAddressValidationTest { + + @Test + public void testDecryptAcceptsMatchingAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // createStandard sets the correct derived address — should decrypt fine + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertEquals("Private key must match", + org.tron.common.utils.ByteArray.toHexString(keyPair.getPrivateKey()), + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptRejectsSpoofedAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // Tamper with the address to simulate a spoofed keystore + walletFile.setAddress("TTamperedAddressXXXXXXXXXXXXXXXXXX"); + + try { + Wallet.decrypt(password, walletFile, true); + fail("Expected CipherException due to address mismatch"); + } catch (CipherException e) { + assertTrue("Error should mention address mismatch, got: " + e.getMessage(), + e.getMessage().contains("address mismatch")); + } + } + + @Test + public void testDecryptAllowsNullAddress() throws Exception { + // Ethereum-style keystores may not include the address field — should still decrypt + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + walletFile.setAddress(null); + + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertNotNull(recovered); + assertEquals(org.tron.common.utils.ByteArray.toHexString(keyPair.getPrivateKey()), + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptAllowsEmptyAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + walletFile.setAddress(""); + + // Empty-string address is treated as absent (no validation) + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertNotNull(recovered); + } + + @Test + public void testDecryptRejectsSpoofedAddressSm2() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), false); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + walletFile.setAddress("TSpoofedSm2Addr123456789XXXXXXXX"); + + try { + Wallet.decrypt(password, walletFile, false); + fail("Expected CipherException due to address mismatch on SM2"); + } catch (CipherException e) { + assertTrue(e.getMessage().contains("address mismatch")); + } + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java new file mode 100644 index 00000000000..83c7096665b --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java @@ -0,0 +1,389 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +public class WalletFilePojoTest { + + @Test + public void testWalletFileGettersSetters() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setId("uuid-123"); + wf.setVersion(3); + WalletFile.Crypto c = new WalletFile.Crypto(); + wf.setCrypto(c); + + assertEquals("TAddr", wf.getAddress()); + assertEquals("uuid-123", wf.getId()); + assertEquals(3, wf.getVersion()); + assertEquals(c, wf.getCrypto()); + } + + @Test + public void testWalletFileCryptoV1Setter() { + WalletFile wf = new WalletFile(); + WalletFile.Crypto c = new WalletFile.Crypto(); + wf.setCryptoV1(c); + assertEquals(c, wf.getCrypto()); + } + + @Test + public void testWalletFileEqualsAllBranches() { + WalletFile a = new WalletFile(); + a.setAddress("TAddr"); + a.setId("id1"); + a.setVersion(3); + WalletFile.Crypto c = new WalletFile.Crypto(); + a.setCrypto(c); + + WalletFile b = new WalletFile(); + b.setAddress("TAddr"); + b.setId("id1"); + b.setVersion(3); + b.setCrypto(c); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + // Different address + b.setAddress("TOther"); + assertNotEquals(a, b); + b.setAddress("TAddr"); + + // Different id + b.setId("id2"); + assertNotEquals(a, b); + b.setId("id1"); + + // Different version + b.setVersion(4); + assertNotEquals(a, b); + b.setVersion(3); + + // Different crypto + b.setCrypto(new WalletFile.Crypto()); + // Still equal since Cryptos are equal (both empty) + assertEquals(a, b); + + // Null fields + WalletFile empty = new WalletFile(); + WalletFile empty2 = new WalletFile(); + assertEquals(empty, empty2); + assertEquals(empty.hashCode(), empty2.hashCode()); + + // One side null + empty2.setAddress("X"); + assertNotEquals(empty, empty2); + } + + @Test + public void testCryptoGettersSetters() { + WalletFile.Crypto c = new WalletFile.Crypto(); + c.setCipher("aes-128-ctr"); + c.setCiphertext("ciphertext"); + c.setKdf("scrypt"); + c.setMac("mac-value"); + + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("ivvalue"); + c.setCipherparams(cp); + + WalletFile.ScryptKdfParams kp = new WalletFile.ScryptKdfParams(); + c.setKdfparams(kp); + + assertEquals("aes-128-ctr", c.getCipher()); + assertEquals("ciphertext", c.getCiphertext()); + assertEquals("scrypt", c.getKdf()); + assertEquals("mac-value", c.getMac()); + assertEquals(cp, c.getCipherparams()); + assertEquals(kp, c.getKdfparams()); + } + + @Test + public void testCryptoEqualsAllBranches() { + WalletFile.Crypto a = new WalletFile.Crypto(); + a.setCipher("c1"); + a.setCiphertext("txt"); + a.setKdf("kdf"); + a.setMac("mac"); + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("iv"); + a.setCipherparams(cp); + WalletFile.Aes128CtrKdfParams kp = new WalletFile.Aes128CtrKdfParams(); + a.setKdfparams(kp); + + WalletFile.Crypto b = new WalletFile.Crypto(); + b.setCipher("c1"); + b.setCiphertext("txt"); + b.setKdf("kdf"); + b.setMac("mac"); + b.setCipherparams(cp); + b.setKdfparams(kp); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + // cipher differs + b.setCipher("c2"); + assertNotEquals(a, b); + b.setCipher("c1"); + + // ciphertext differs + b.setCiphertext("other"); + assertNotEquals(a, b); + b.setCiphertext("txt"); + + // kdf differs + b.setKdf("other"); + assertNotEquals(a, b); + b.setKdf("kdf"); + + // mac differs + b.setMac("other"); + assertNotEquals(a, b); + b.setMac("mac"); + + // cipherparams differs + WalletFile.CipherParams cp2 = new WalletFile.CipherParams(); + cp2.setIv("other"); + b.setCipherparams(cp2); + assertNotEquals(a, b); + b.setCipherparams(cp); + + // kdfparams differs + WalletFile.Aes128CtrKdfParams kp2 = new WalletFile.Aes128CtrKdfParams(); + kp2.setC(5); + b.setKdfparams(kp2); + assertNotEquals(a, b); + } + + @Test + public void testCryptoNullFields() { + WalletFile.Crypto a = new WalletFile.Crypto(); + WalletFile.Crypto b = new WalletFile.Crypto(); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + a.setCipher("x"); + assertNotEquals(a, b); + } + + @Test + public void testCipherParamsGettersSetters() { + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("ivvalue"); + assertEquals("ivvalue", cp.getIv()); + } + + @Test + public void testCipherParamsEquals() { + WalletFile.CipherParams a = new WalletFile.CipherParams(); + WalletFile.CipherParams b = new WalletFile.CipherParams(); + assertEquals(a, b); + a.setIv("iv"); + assertNotEquals(a, b); + b.setIv("iv"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.setIv("other"); + assertNotEquals(a, b); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + } + + @Test + public void testAes128CtrKdfParamsAllAccessors() { + WalletFile.Aes128CtrKdfParams p = new WalletFile.Aes128CtrKdfParams(); + p.setDklen(32); + p.setC(262144); + p.setPrf("hmac-sha256"); + p.setSalt("saltvalue"); + + assertEquals(32, p.getDklen()); + assertEquals(262144, p.getC()); + assertEquals("hmac-sha256", p.getPrf()); + assertEquals("saltvalue", p.getSalt()); + } + + @Test + public void testAes128CtrKdfParamsEquals() { + WalletFile.Aes128CtrKdfParams a = new WalletFile.Aes128CtrKdfParams(); + a.setDklen(32); + a.setC(262144); + a.setPrf("hmac-sha256"); + a.setSalt("salt"); + + WalletFile.Aes128CtrKdfParams b = new WalletFile.Aes128CtrKdfParams(); + b.setDklen(32); + b.setC(262144); + b.setPrf("hmac-sha256"); + b.setSalt("salt"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + b.setDklen(64); + assertNotEquals(a, b); + b.setDklen(32); + + b.setC(1); + assertNotEquals(a, b); + b.setC(262144); + + b.setPrf("other"); + assertNotEquals(a, b); + b.setPrf("hmac-sha256"); + + b.setSalt("other"); + assertNotEquals(a, b); + b.setSalt("salt"); + + // null fields + WalletFile.Aes128CtrKdfParams x = new WalletFile.Aes128CtrKdfParams(); + WalletFile.Aes128CtrKdfParams y = new WalletFile.Aes128CtrKdfParams(); + assertEquals(x, y); + x.setPrf("x"); + assertNotEquals(x, y); + } + + @Test + public void testScryptKdfParamsAllAccessors() { + WalletFile.ScryptKdfParams p = new WalletFile.ScryptKdfParams(); + p.setDklen(32); + p.setN(262144); + p.setP(1); + p.setR(8); + p.setSalt("saltvalue"); + + assertEquals(32, p.getDklen()); + assertEquals(262144, p.getN()); + assertEquals(1, p.getP()); + assertEquals(8, p.getR()); + assertEquals("saltvalue", p.getSalt()); + } + + @Test + public void testScryptKdfParamsEquals() { + WalletFile.ScryptKdfParams a = new WalletFile.ScryptKdfParams(); + a.setDklen(32); + a.setN(262144); + a.setP(1); + a.setR(8); + a.setSalt("salt"); + + WalletFile.ScryptKdfParams b = new WalletFile.ScryptKdfParams(); + b.setDklen(32); + b.setN(262144); + b.setP(1); + b.setR(8); + b.setSalt("salt"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + b.setDklen(64); + assertNotEquals(a, b); + b.setDklen(32); + + b.setN(1); + assertNotEquals(a, b); + b.setN(262144); + + b.setP(2); + assertNotEquals(a, b); + b.setP(1); + + b.setR(16); + assertNotEquals(a, b); + b.setR(8); + + b.setSalt("other"); + assertNotEquals(a, b); + + // null salt + WalletFile.ScryptKdfParams x = new WalletFile.ScryptKdfParams(); + WalletFile.ScryptKdfParams y = new WalletFile.ScryptKdfParams(); + assertEquals(x, y); + x.setSalt("x"); + assertNotEquals(x, y); + } + + @Test + public void testJsonDeserializeWithScryptKdf() throws Exception { + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"id\":\"uuid\"," + + "\"crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"ciphertext\":\"ct\"," + + " \"cipherparams\":{\"iv\":\"iv\"}," + + " \"kdf\":\"scrypt\"," + + " \"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"salt\"}," + + " \"mac\":\"mac\"" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertEquals("TAddr", wf.getAddress()); + assertEquals(3, wf.getVersion()); + assertNotNull(wf.getCrypto()); + assertNotNull(wf.getCrypto().getKdfparams()); + assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.ScryptKdfParams); + } + + @Test + public void testJsonDeserializeWithAes128Kdf() throws Exception { + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"ciphertext\":\"ct\"," + + " \"cipherparams\":{\"iv\":\"iv\"}," + + " \"kdf\":\"pbkdf2\"," + + " \"kdfparams\":{\"dklen\":32,\"c\":262144,\"prf\":\"hmac-sha256\",\"salt\":\"salt\"}," + + " \"mac\":\"mac\"" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertNotNull(wf.getCrypto().getKdfparams()); + assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.Aes128CtrKdfParams); + } + + @Test + public void testJsonDeserializeCryptoV1Field() throws Exception { + // Legacy files may use "Crypto" instead of "crypto" + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"Crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"kdf\":\"scrypt\"," + + " \"kdfparams\":{\"dklen\":32,\"n\":1,\"p\":1,\"r\":8,\"salt\":\"s\"}" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertNotNull(wf.getCrypto()); + assertEquals("aes-128-ctr", wf.getCrypto().getCipher()); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java b/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java new file mode 100644 index 00000000000..3028d2a7799 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java @@ -0,0 +1,77 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertArrayEquals; + +import java.security.SecureRandom; +import org.junit.Test; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; + +/** + * Property-based roundtrip tests: decrypt(encrypt(privateKey, password)) == privateKey. + * Uses randomized inputs via loop instead of jqwik to avoid dependency verification overhead. + */ +public class WalletPropertyTest { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String CHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + @Test + public void encryptDecryptRoundtripLight() throws Exception { + for (int i = 0; i < 100; i++) { + String password = randomPassword(6, 32); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + + WalletFile walletFile = Wallet.createLight(password, keyPair); + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + + assertArrayEquals("Roundtrip failed at iteration " + i, + originalKey, recovered.getPrivateKey()); + } + } + + @Test(timeout = 120000) + public void encryptDecryptRoundtripStandard() throws Exception { + // Fewer iterations for standard scrypt (slow, ~10s each) + for (int i = 0; i < 2; i++) { + String password = randomPassword(6, 16); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + + WalletFile walletFile = Wallet.createStandard(password, keyPair); + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + + assertArrayEquals("Standard roundtrip failed at iteration " + i, + originalKey, recovered.getPrivateKey()); + } + } + + @Test + public void wrongPasswordFailsDecrypt() throws Exception { + for (int i = 0; i < 50; i++) { + String password = randomPassword(6, 16); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createLight(password, keyPair); + + try { + Wallet.decrypt(password + "X", walletFile, true); + throw new AssertionError("Expected CipherException at iteration " + i); + } catch (CipherException e) { + // Expected + } + } + } + + private String randomPassword(int minLen, int maxLen) { + int len = minLen + RANDOM.nextInt(maxLen - minLen + 1); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length()))); + } + return sb.toString(); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java new file mode 100644 index 00000000000..64752b9ca49 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java @@ -0,0 +1,167 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies that {@link WalletUtils#inputPassword()} preserves the full + * password including internal whitespace on the non-TTY (stdin) path, + * and that {@link WalletUtils#stripPasswordLine(String)} handles all + * edge cases correctly. + * + *

Previously the non-TTY path applied {@code trim() + split("\\s+")[0]} + * which silently truncated passphrases like "correct horse battery staple" + * to "correct" when piped via stdin. This test locks in the fix. + */ +public class WalletUtilsInputPasswordTest { + + private InputStream originalIn; + + @Before + public void saveStdin() { + originalIn = System.in; + // Clear the cached Scanner so each test binds to its own System.in + WalletUtils.resetSharedStdinScanner(); + } + + @After + public void restoreStdin() { + System.setIn(originalIn); + WalletUtils.resetSharedStdinScanner(); + } + + // ---------- inputPassword() behavioral tests ---------- + + @Test(timeout = 5000) + public void testInputPasswordPreservesInternalWhitespace() { + System.setIn(new ByteArrayInputStream( + "correct horse battery staple\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Password with internal whitespace must be preserved intact", + "correct horse battery staple", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordPreservesTabs() { + System.setIn(new ByteArrayInputStream( + "pass\tw0rd\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Internal tabs must be preserved", "pass\tw0rd", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordStripsTrailingCr() { + // Windows line endings + System.setIn(new ByteArrayInputStream( + "password123\r\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Trailing \\r must be stripped", "password123", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordStripsBom() { + System.setIn(new ByteArrayInputStream( + "\uFEFFpassword123\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("UTF-8 BOM must be stripped from the start", "password123", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordPreservesLeadingAndTrailingSpaces() { + // The legacy bug also called trim(); post-fix, spaces at the edges + // are part of the password. Callers that want to trim should do so + // themselves with full knowledge. + System.setIn(new ByteArrayInputStream( + " with spaces \n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Leading and trailing spaces are part of the password", + " with spaces ", pw); + } + + @Test(timeout = 10000) + public void testInputPassword2TwicePipedPreservesInternalWhitespace() { + // M1: verifies the double-read path (inputPassword2Twice → inputPassword() + // called twice) works correctly when both lines arrive on the same + // piped stdin. Guards against regressions from Scanner lifecycle issues + // where a newly-constructed Scanner could miss bytes buffered by an + // earlier Scanner on the same InputStream. + System.setIn(new ByteArrayInputStream( + ("correct horse battery staple\n" + + "correct horse battery staple\n").getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword2Twice(); + + assertEquals("Full passphrase must survive the double-read path", + "correct horse battery staple", pw); + } + + // ---------- stripPasswordLine() direct unit tests (M3) ---------- + + @Test + public void testStripPasswordLineNull() { + assertNull(WalletUtils.stripPasswordLine(null)); + } + + @Test + public void testStripPasswordLineEmpty() { + assertEquals("", WalletUtils.stripPasswordLine("")); + } + + @Test + public void testStripPasswordLineOnlyBom() { + assertEquals("", WalletUtils.stripPasswordLine("\uFEFF")); + } + + @Test + public void testStripPasswordLineOnlyLineTerminators() { + assertEquals("", WalletUtils.stripPasswordLine("\r\n\r\n")); + } + + @Test + public void testStripPasswordLineBomThenTerminator() { + assertEquals("", WalletUtils.stripPasswordLine("\uFEFF\r\n")); + } + + @Test + public void testStripPasswordLineBomAndInternalWhitespace() { + assertEquals("with spaces", + WalletUtils.stripPasswordLine("\uFEFFwith spaces\r\n")); + } + + @Test + public void testStripPasswordLineNoChange() { + assertEquals("password", WalletUtils.stripPasswordLine("password")); + } + + @Test + public void testStripPasswordLineTrailingLf() { + assertEquals("password", WalletUtils.stripPasswordLine("password\n")); + } + + @Test + public void testStripPasswordLineTrailingCr() { + assertEquals("password", WalletUtils.stripPasswordLine("password\r")); + } + + @Test + public void testStripPasswordLineMultipleTrailing() { + assertEquals("password", WalletUtils.stripPasswordLine("password\r\n\r\n")); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java b/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java new file mode 100644 index 00000000000..f273d751961 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java @@ -0,0 +1,204 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; + +/** + * Verifies that {@link WalletUtils#generateWalletFile} and + * {@link WalletUtils#writeWalletFile} produce keystore files with + * owner-only permissions (0600) atomically, leaving no temp files behind. + * + *

Tests use light scrypt (useFullScrypt=false) where possible because + * they validate filesystem behavior, not the KDF parameters. + */ +public class WalletUtilsWriteTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private static WalletFile lightWalletFile(String password) throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + return Wallet.createLight(password, keyPair); + } + + @Test + public void testGenerateWalletFileCreatesOwnerOnlyFile() throws Exception { + Assume.assumeTrue("POSIX permissions test", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("gen-perms"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + File created = new File(dir, fileName); + assertTrue(created.exists()); + + Set perms = Files.getPosixFilePermissions(created.toPath()); + assertEquals("Keystore must have owner-only permissions (rw-------)", + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testGenerateWalletFileLeavesNoTempFile() throws Exception { + File dir = tempFolder.newFolder("gen-no-temp"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("No temp files should remain after generation", 0, tempFiles.length); + } + + @Test + public void testGenerateWalletFileLightScrypt() throws Exception { + File dir = tempFolder.newFolder("gen-light"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + assertNotNull(fileName); + assertTrue(fileName.endsWith(".json")); + assertTrue(new File(dir, fileName).exists()); + } + + @Test + public void testWriteWalletFileOwnerOnly() throws Exception { + Assume.assumeTrue("POSIX permissions test", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("write-perms"); + WalletFile wf = lightWalletFile("password123"); + File destination = new File(dir, "out.json"); + + WalletUtils.writeWalletFile(wf, destination); + + assertTrue(destination.exists()); + Set perms = Files.getPosixFilePermissions(destination.toPath()); + assertEquals("Keystore must have owner-only permissions (rw-------)", + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testWriteWalletFileReplacesExisting() throws Exception { + File dir = tempFolder.newFolder("write-replace"); + WalletFile wf1 = lightWalletFile("password123"); + WalletFile wf2 = lightWalletFile("password123"); + File destination = new File(dir, "out.json"); + + WalletUtils.writeWalletFile(wf1, destination); + WalletUtils.writeWalletFile(wf2, destination); + + assertTrue("Destination exists after replace", destination.exists()); + WalletFile reread = new com.fasterxml.jackson.databind.ObjectMapper() + .readValue(destination, WalletFile.class); + assertEquals("Replaced file should have wf2's address", + wf2.getAddress(), reread.getAddress()); + } + + @Test + public void testWriteWalletFileLeavesNoTempFile() throws Exception { + File dir = tempFolder.newFolder("write-no-temp"); + WalletFile wf = lightWalletFile("password123"); + File destination = new File(dir, "final.json"); + + WalletUtils.writeWalletFile(wf, destination); + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("No temp files should remain", 0, tempFiles.length); + } + + @Test + public void testWriteWalletFileCreatesParentDirectories() throws Exception { + File base = tempFolder.newFolder("write-nested"); + File destination = new File(base, "a/b/c/out.json"); + assertFalse("Parent dir does not exist yet", destination.getParentFile().exists()); + + WalletFile wf = lightWalletFile("password123"); + WalletUtils.writeWalletFile(wf, destination); + + assertTrue("Destination written", destination.exists()); + } + + @Test + public void testWriteWalletFileCleansUpTempOnFailure() throws Exception { + // Force failure by making the destination a directory — Files.move will fail + // because the source is a file. The temp file must be cleaned up. + File dir = tempFolder.newFolder("write-fail"); + File destinationAsDir = new File(dir, "blocking-dir"); + assertTrue("Setup: blocking dir created", destinationAsDir.mkdir()); + // Put a file inside so Files.move with REPLACE_EXISTING fails (non-empty dir). + assertTrue("Setup: block file", new File(destinationAsDir, "blocker").createNewFile()); + + WalletFile wf = lightWalletFile("password123"); + + try { + WalletUtils.writeWalletFile(wf, destinationAsDir); + fail("Expected IOException because destination is a non-empty directory"); + } catch (IOException expected) { + // Expected + } + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("Temp file must be cleaned up on failure", 0, tempFiles.length); + } + + // ---------- loadCredentials symlink behavior ---------- + + @Test + public void testLoadCredentialsFollowsSymlinkButWarns() throws Exception { + Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File realDir = tempFolder.newFolder("load-symlink-target"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + String realName = WalletUtils.generateWalletFile("password123", keyPair, realDir, false); + File realKeystore = new File(realDir, realName); + + File linkDir = tempFolder.newFolder("load-symlink-link"); + File symlink = new File(linkDir, "witness.json"); + Files.createSymbolicLink(symlink.toPath(), realKeystore.toPath()); + + // Should NOT throw — Lighthouse-style: follow the symlink, log a warning + // for the operator. Hard-rejecting would silently break legitimate SR + // deployments that organize keystores via symlinks. + Credentials creds = + WalletUtils.loadCredentials("password123", symlink, true); + assertNotNull(creds.getAddress()); + } + + @Test + public void testLoadCredentialsAcceptsRegularFile() throws Exception { + File dir = tempFolder.newFolder("load-ok"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + Credentials creds = + WalletUtils.loadCredentials("password123", new File(dir, fileName), true); + assertNotNull(creds.getAddress()); + } +} diff --git a/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java new file mode 100644 index 00000000000..860980d21e5 --- /dev/null +++ b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java @@ -0,0 +1,147 @@ +package org.tron.program; + +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +/** + * Verifies the deprecated --keystore-factory CLI. + */ +public class KeystoreFactoryDeprecationTest { + + private PrintStream originalOut; + private PrintStream originalErr; + private InputStream originalIn; + + @Before + public void setup() { + originalOut = System.out; + originalErr = System.err; + originalIn = System.in; + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + } + + @After + public void teardown() throws Exception { + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + Args.clearParam(); + // Clean up Wallet dir + File wallet = new File("Wallet"); + if (wallet.exists()) { + if (wallet.isDirectory() && wallet.listFiles() != null) { + for (File f : wallet.listFiles()) { + f.delete(); + } + } + wallet.delete(); + } + } + + @Test(timeout = 10000) + public void testDeprecationWarningPrinted() throws Exception { + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errContent)); + System.setIn(new ByteArrayInputStream("exit\n".getBytes())); + + KeystoreFactory.start(); + + String errOutput = errContent.toString("UTF-8"); + assertTrue("Should contain deprecation warning", + errOutput.contains("--keystore-factory is deprecated")); + assertTrue("Should point to Toolkit.jar", + errOutput.contains("Toolkit.jar keystore")); + } + + @Test(timeout = 10000) + public void testHelpCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("help\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should show legacy commands", out.contains("GenKeystore")); + assertTrue("Should show ImportPrivateKey", out.contains("ImportPrivateKey")); + } + + @Test(timeout = 10000) + public void testInvalidCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("badcommand\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should report invalid cmd", + out.contains("Invalid cmd: badcommand")); + } + + @Test(timeout = 10000) + public void testEmptyLineSkipped() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("\n\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should exit cleanly", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testQuitCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("quit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Quit should terminate", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testGenKeystoreTriggersError() throws Exception { + // genkeystore reads password via a nested Scanner, which conflicts + // with the outer Scanner and throws "No line found". The error is + // caught and logged, and the REPL continues. + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("genKeystore should prompt for password", + out.contains("Please input password")); + assertTrue("REPL should continue to exit", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testImportPrivateKeyTriggersPrompt() throws Exception { + // importprivatekey reads via nested Scanner — same limitation as above, + // but we at least hit the dispatch logic. + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("importprivatekey\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("importprivatekey should prompt for key", + out.contains("Please input private key")); + } +} diff --git a/framework/src/test/java/org/tron/program/SupplementTest.java b/framework/src/test/java/org/tron/program/SupplementTest.java index 38a1b8426dd..483922cf8c5 100644 --- a/framework/src/test/java/org/tron/program/SupplementTest.java +++ b/framework/src/test/java/org/tron/program/SupplementTest.java @@ -27,7 +27,6 @@ import org.tron.core.config.args.Args; import org.tron.core.services.http.HttpSelfFormatFieldName; import org.tron.core.store.StorageRowStore; -import org.tron.keystore.WalletUtils; public class SupplementTest extends BaseTest { @@ -54,12 +53,6 @@ public void testGet() throws Exception { String p = dbPath + File.separator; dbBackupConfig.initArgs(true, p + "propPath", p + "bak1path/", p + "bak2path/", 1); - WalletUtils.generateFullNewWalletFile("123456", new File(dbPath)); - WalletUtils.generateLightNewWalletFile("123456", new File(dbPath)); - WalletUtils.getDefaultKeyDirectory(); - WalletUtils.getTestnetKeyDirectory(); - WalletUtils.getMainnetKeyDirectory(); - Value value = new Value(new byte[]{1}); value.asBytes(); value = new Value(1); diff --git a/plugins/README.md b/plugins/README.md index db25811882f..b6540beef1a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -143,3 +143,78 @@ NOTE: large db may GC overhead limit exceeded. - ``: Source path for database. Default: output-directory/database - `--db`: db name. - `-h | --help`: provide the help info + +## Keystore + +Keystore provides commands for managing account keystore files (Web3 Secret Storage format). + +> **Migrating from `--keystore-factory`**: The legacy `FullNode.jar --keystore-factory` interactive mode is deprecated. Use the Toolkit keystore commands below instead. The mapping is: +> - `GenKeystore` → `keystore new` +> - `ImportPrivateKey` → `keystore import` +> - (new) `keystore list` — list all keystores in a directory +> - (new) `keystore update` — change the password of a keystore + +### Subcommands + +#### keystore new + +Generate a new keystore file with a random keypair. + +```shell script +# full command + java -jar Toolkit.jar keystore new [-h] [--keystore-dir=

] [--password-file=] [--sm2] [--json] +# examples + java -jar Toolkit.jar keystore new # interactive prompt + java -jar Toolkit.jar keystore new --keystore-dir /data/keystores # custom directory + java -jar Toolkit.jar keystore new --password-file pass.txt --json # non-interactive with JSON output +``` + +#### keystore import + +Import a private key into a new keystore file. + +```shell script +# full command + java -jar Toolkit.jar keystore import [-h] [--keystore-dir=] [--password-file=] [--key-file=] [--sm2] [--force] [--json] +# examples + java -jar Toolkit.jar keystore import # interactive prompt + java -jar Toolkit.jar keystore import --key-file key.txt --json # from file with JSON output +``` + +#### keystore list + +List all keystore files in a directory. + +```shell script +# full command + java -jar Toolkit.jar keystore list [-h] [--keystore-dir=] [--json] +# examples + java -jar Toolkit.jar keystore list # list default ./Wallet directory + java -jar Toolkit.jar keystore list --keystore-dir /data/keystores # custom directory +``` + +> **Note**: `list` displays the `address` field as declared in each keystore JSON without decrypting the file. A tampered keystore can claim an address that does not correspond to its encrypted private key. The address is only cryptographically verified at decryption time (e.g. by `update` or by tools that load the credentials). Only trust keystores from sources you control. + +#### keystore update + +Change the password of a keystore file. + +```shell script +# full command + java -jar Toolkit.jar keystore update [-h]
[--keystore-dir=] [--password-file=] [--sm2] [--json] +# examples + java -jar Toolkit.jar keystore update TXyz...abc # interactive prompt + java -jar Toolkit.jar keystore update TXyz...abc --keystore-dir /data/ks # custom directory +``` + +When using `--password-file` with `update`, the file must contain exactly two lines: the **current** password on the first line and the **new** password on the second line. Both leading/trailing whitespace within a line is preserved (passphrases with spaces are supported). + +### Common Options + +- `--keystore-dir`: Keystore directory, default: `./Wallet`. +- `--password-file`: Read password from a file instead of interactive prompt. For `keystore update`, the file must contain exactly two lines (current password, then new password). +- `--key-file`: Read the private key (hex, with or without `0x` prefix) from a file instead of the interactive prompt (`keystore import` only). +- `--force`: For `keystore import`, allow importing a private key whose address already has a keystore in the directory (creates an additional file). +- `--sm2`: Use SM2 algorithm instead of ECDSA (for `new` and `import`). +- `--json`: Output in JSON format for scripting. +- `-h | --help`: Provide the help info. diff --git a/plugins/build.gradle b/plugins/build.gradle index 85dcdd2342d..2e358a884a3 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -34,6 +34,12 @@ dependencies { implementation fileTree(dir: 'libs', include: '*.jar') testImplementation project(":framework") testImplementation project(":framework").sourceSets.test.output + implementation(project(":crypto")) { + exclude group: 'io.github.tronprotocol', module: 'libp2p' + exclude group: 'io.prometheus' + exclude group: 'org.aspectj' + exclude group: 'org.apache.httpcomponents' + } implementation group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' implementation group: 'me.tongfei', name: 'progressbar', version: '0.9.3' diff --git a/plugins/src/main/java/common/org/tron/plugins/Keystore.java b/plugins/src/main/java/common/org/tron/plugins/Keystore.java new file mode 100644 index 00000000000..6929bb406ea --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/Keystore.java @@ -0,0 +1,19 @@ +package org.tron.plugins; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "keystore", + mixinStandardHelpOptions = true, + version = "keystore command 1.0", + description = "Manage keystore files for account keys.", + subcommands = {CommandLine.HelpCommand.class, + KeystoreNew.class, + KeystoreImport.class, + KeystoreList.class, + KeystoreUpdate.class + }, + commandListHeading = "%nCommands:%n%nThe most commonly used keystore commands are:%n" +) +public class Keystore { +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java new file mode 100644 index 00000000000..6959a7f8177 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java @@ -0,0 +1,304 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; + +/** + * Shared utilities for keystore CLI commands. + */ +final class KeystoreCliUtils { + + private static final long MAX_FILE_SIZE = 1024; + + /** + * Cap on the size of a single keystore JSON read during directory scans. + * Standard V3 keystores are ~500–700 bytes; 8 KiB leaves headroom for + * unusual scrypt parameter combinations while bounding the memory cost + * of scanning a hostile directory of planted oversized files. + */ + static final long MAX_KEYSTORE_SIZE = 8 * 1024; + + private KeystoreCliUtils() { + } + + /** + * Read a regular file safely without following symbolic links. + * + *

This prevents an attacker who can plant files in a user-supplied + * path from redirecting the read to an arbitrary file on disk (e.g. a + * symlink pointing at {@code /etc/shadow} or a user's SSH private key). + * Also rejects FIFOs, devices and other non-regular files. + * + * @param file the file to read + * @param maxSize maximum acceptable file size in bytes + * @param label human-readable label used in error messages + * @param err writer for diagnostic messages + * @return file bytes, or {@code null} if the file is missing, a symlink, + * not a regular file, or too large (err is written in each case) + */ + static byte[] readRegularFile(File file, long maxSize, String label, PrintWriter err) + throws IOException { + Path path = file.toPath(); + + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } catch (NoSuchFileException e) { + err.println(label + " not found: " + file.getPath()); + return null; + } + + if (attrs.isSymbolicLink()) { + err.println("Refusing to follow symbolic link: " + file.getPath()); + return null; + } + if (!attrs.isRegularFile()) { + err.println("Not a regular file: " + file.getPath()); + return null; + } + if (attrs.size() > maxSize) { + err.println(label + " too large (max " + maxSize + " bytes): " + file.getPath()); + return null; + } + + int size = (int) attrs.size(); + java.util.Set openOptions = new HashSet<>(); + openOptions.add(StandardOpenOption.READ); + openOptions.add(LinkOption.NOFOLLOW_LINKS); + try (SeekableByteChannel ch = Files.newByteChannel(path, openOptions)) { + ByteBuffer buf = ByteBuffer.allocate(size); + while (buf.hasRemaining()) { + if (ch.read(buf) < 0) { + break; + } + } + if (buf.position() < size) { + byte[] actual = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, actual, 0, buf.position()); + return actual; + } + return buf.array(); + } + } + + static String readPassword(File passwordFile, PrintWriter err) throws IOException { + if (passwordFile != null) { + byte[] bytes = readRegularFile(passwordFile, MAX_FILE_SIZE, "Password file", err); + if (bytes == null) { + return null; + } + try { + String password = WalletUtils.stripPasswordLine( + new String(bytes, StandardCharsets.UTF_8)); + // Reject multi-line password files. stripPasswordLine only trims + // trailing terminators; any remaining \n/\r means the file had + // interior line breaks. A common mistake is passing a two-line + // `keystore update` password file to `keystore new` / `import` — + // without this guard the literal "old\nnew" would silently become + // the password, and neither visible line alone would unlock the + // keystore later. + if (password.indexOf('\n') >= 0 || password.indexOf('\r') >= 0) { + err.println("Password file contains multiple lines; provide a " + + "single-line password (the `keystore update` two-line " + + "format is not accepted here)."); + return null; + } + if (!WalletUtils.passwordValid(password)) { + err.println("Invalid password: must be at least 6 characters."); + return null; + } + return password; + } finally { + Arrays.fill(bytes, (byte) 0); + } + } + + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --password-file to provide password."); + return null; + } + + char[] pwd1 = console.readPassword("Enter password: "); + if (pwd1 == null) { + err.println("Password input cancelled."); + return null; + } + char[] pwd2 = console.readPassword("Confirm password: "); + if (pwd2 == null) { + Arrays.fill(pwd1, '\0'); + err.println("Password input cancelled."); + return null; + } + try { + if (!Arrays.equals(pwd1, pwd2)) { + err.println("Passwords do not match."); + return null; + } + String password = new String(pwd1); + if (!WalletUtils.passwordValid(password)) { + err.println("Invalid password: must be at least 6 characters."); + return null; + } + return password; + } finally { + Arrays.fill(pwd1, '\0'); + Arrays.fill(pwd2, '\0'); + } + } + + static void ensureDirectory(File dir) throws IOException { + Path path = dir.toPath(); + if (Files.exists(path) && !Files.isDirectory(path)) { + throw new IOException( + "Path exists but is not a directory: " + dir.getAbsolutePath()); + } + Files.createDirectories(path); + } + + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + static ObjectMapper mapper() { + return MAPPER; + } + + static void printJson(PrintWriter out, PrintWriter err, Map fields) { + try { + out.println(MAPPER.writeValueAsString(fields)); + } catch (Exception e) { + err.println("Error writing JSON output"); + } + } + + static Map jsonMap(String... keyValues) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length - 1; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + static boolean checkFileExists(File file, String label, PrintWriter err) { + if (file != null && !file.exists()) { + err.println(label + " not found: " + file.getPath()); + return false; + } + return true; + } + + /** + * Read the bytes of a keystore-directory entry, refusing to follow + * symbolic links and rejecting non-regular files. Returns {@code null} + * (with a warning to {@code err}) when the entry should be skipped. + * + *

Unlike {@code Files.readAttributes(...) + MAPPER.readValue(file, ...)}, + * this opens the channel with {@link LinkOption#NOFOLLOW_LINKS} so the + * {@code O_NOFOLLOW} flag is enforced atomically by the kernel at + * {@code open(2)} — closing the TOCTOU window between an lstat-style + * check and a follow-symlink {@code FileInputStream} open. The caller + * then deserializes the bytes via {@code ObjectMapper.readValue(byte[], + * Class)}. + * + *

Files larger than {@link #MAX_KEYSTORE_SIZE} are skipped to bound + * memory cost when scanning a hostile or oversized directory. + */ + static byte[] readKeystoreFile(File file, PrintWriter err) { + Path path = file.toPath(); + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + return null; + } + if (attrs.isSymbolicLink()) { + err.println("Warning: skipping symbolic link: " + file.getName()); + return null; + } + if (!attrs.isRegularFile()) { + err.println("Warning: skipping non-regular file: " + file.getName()); + return null; + } + if (attrs.size() > MAX_KEYSTORE_SIZE) { + err.println("Warning: skipping oversized file (>" + MAX_KEYSTORE_SIZE + + " bytes): " + file.getName()); + return null; + } + + int size = (int) attrs.size(); + java.util.Set openOptions = new HashSet<>(); + openOptions.add(StandardOpenOption.READ); + openOptions.add(LinkOption.NOFOLLOW_LINKS); + try (SeekableByteChannel ch = Files.newByteChannel(path, openOptions)) { + ByteBuffer buf = ByteBuffer.allocate(size); + while (buf.hasRemaining()) { + if (ch.read(buf) < 0) { + break; + } + } + if (buf.position() < size) { + byte[] actual = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, actual, 0, buf.position()); + return actual; + } + return buf.array(); + } catch (IOException e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + return null; + } + } + + static void printSecurityTips(PrintWriter out, String address, String fileName) { + out.println(); + out.println("Public address of the key: " + address); + out.println("Path of the secret key file: " + fileName); + out.println(); + out.println( + "- You can share your public address with anyone." + + " Others need it to interact with you."); + out.println( + "- You must NEVER share the secret key with anyone!" + + " The key controls access to your funds!"); + out.println( + "- You must BACKUP your key file!" + + " Without the key, it's impossible to access account funds!"); + out.println( + "- You must REMEMBER your password!" + + " Without the password, it's impossible to decrypt the key!"); + } + + /** + * Check if a WalletFile represents a decryptable V3 keystore. + * Delegates to {@link Wallet#isValidKeystoreFile(WalletFile)} so the + * discovery predicate stays in sync with decryption-time validation — + * a JSON stub with empty or unsupported cipher/KDF is rejected here + * rather than silently showing up as a "keystore" and failing later. + */ + static boolean isValidKeystoreFile(WalletFile wf) { + return org.tron.keystore.Wallet.isValidKeystoreFile(wf); + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java new file mode 100644 index 00000000000..67c8e6bc4c6 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java @@ -0,0 +1,188 @@ +package org.tron.plugins; + +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.apache.commons.lang3.StringUtils; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "import", + mixinStandardHelpOptions = true, + description = "Import a private key into a new keystore file.") +public class KeystoreImport implements Callable { + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--key-file"}, + description = "Read private key from file instead of interactive prompt") + private File keyFile; + + @Option(names = {"--password-file"}, + description = "Read password from file instead of interactive prompt") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Option(names = {"--force"}, + description = "Allow import even if address already exists") + private boolean force; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + try { + if (!KeystoreCliUtils.checkFileExists(keyFile, "Key file", err)) { + return 1; + } + KeystoreCliUtils.ensureDirectory(keystoreDir); + + String privateKey = readPrivateKey(err); + if (privateKey == null) { + return 1; + } + + if (privateKey.startsWith("0x") || privateKey.startsWith("0X")) { + privateKey = privateKey.substring(2); + } + if (!isValidPrivateKey(privateKey)) { + err.println("Invalid private key: must be 64 hex characters."); + return 1; + } + + String password = KeystoreCliUtils.readPassword(passwordFile, err); + if (password == null) { + return 1; + } + + boolean ecKey = !sm2; + SignInterface keyPair; + try { + keyPair = SignUtils.fromPrivate( + ByteArray.fromHexString(privateKey), ecKey); + } catch (Exception e) { + err.println("Invalid private key: not a valid key" + + " for the selected algorithm."); + return 1; + } + String address = Credentials.create(keyPair).getAddress(); + String existingFile = findExistingKeystore(keystoreDir, address, err); + if (existingFile != null && !force) { + err.println("Keystore for address " + address + + " already exists: " + existingFile + + ". Use --force to import anyway."); + return 1; + } + String fileName = WalletUtils.generateWalletFile( + password, keyPair, keystoreDir, true); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", address, "file", fileName)); + } else { + out.println("Imported keystore successfully"); + KeystoreCliUtils.printSecurityTips(out, address, + new File(keystoreDir, fileName).getPath()); + } + return 0; + } catch (CipherException e) { + err.println("Encryption error: " + e.getMessage()); + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } + + private String readPrivateKey(PrintWriter err) throws IOException { + if (keyFile != null) { + byte[] bytes = KeystoreCliUtils.readRegularFile(keyFile, 1024, "Key file", err); + if (bytes == null) { + return null; + } + try { + return new String(bytes, StandardCharsets.UTF_8).trim(); + } finally { + Arrays.fill(bytes, (byte) 0); + } + } + + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --key-file to provide private key."); + return null; + } + + char[] key = console.readPassword("Enter private key (hex): "); + if (key == null) { + err.println("Input cancelled."); + return null; + } + try { + return new String(key); + } finally { + Arrays.fill(key, '\0'); + } + } + + private static final java.util.regex.Pattern HEX_PATTERN = + java.util.regex.Pattern.compile("[0-9a-fA-F]{64}"); + + private boolean isValidPrivateKey(String key) { + return !StringUtils.isEmpty(key) && HEX_PATTERN.matcher(key).matches(); + } + + private String findExistingKeystore(File dir, String address, PrintWriter err) { + if (!dir.exists() || !dir.isDirectory()) { + return null; + } + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + if (files == null) { + return null; + } + com.fasterxml.jackson.databind.ObjectMapper mapper = + KeystoreCliUtils.mapper(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile wf = mapper.readValue(bytes, WalletFile.class); + if (KeystoreCliUtils.isValidKeystoreFile(wf) + && address.equals(wf.getAddress())) { + return file.getName(); + } + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + return null; + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java new file mode 100644 index 00000000000..e7218be9fbf --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java @@ -0,0 +1,110 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import org.tron.keystore.WalletFile; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "list", + mixinStandardHelpOptions = true, + description = "List all keystore files in a directory.") +public class KeystoreList implements Callable { + + private static final ObjectMapper MAPPER = KeystoreCliUtils.mapper(); + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + + if (!keystoreDir.exists() || !keystoreDir.isDirectory()) { + if (json) { + return printEmptyJson(out, err); + } else { + out.println("No keystores found in: " + keystoreDir.getAbsolutePath()); + } + return 0; + } + + File[] files = keystoreDir.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null || files.length == 0) { + if (json) { + return printEmptyJson(out, err); + } else { + out.println("No keystores found in: " + keystoreDir.getAbsolutePath()); + } + return 0; + } + + List> entries = new ArrayList<>(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile walletFile = MAPPER.readValue(bytes, WalletFile.class); + if (!KeystoreCliUtils.isValidKeystoreFile(walletFile)) { + continue; + } + Map entry = new LinkedHashMap<>(); + entry.put("address", walletFile.getAddress()); + entry.put("file", file.getName()); + entries.add(entry); + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + + if (json) { + try { + Map result = new LinkedHashMap<>(); + result.put("keystores", entries); + out.println(MAPPER.writeValueAsString(result)); + } catch (Exception e) { + err.println("Error writing JSON output"); + return 1; + } + } else if (entries.isEmpty()) { + out.println("No valid keystores found in: " + keystoreDir.getAbsolutePath()); + } else { + for (Map entry : entries) { + out.printf("%-45s %s%n", entry.get("address"), entry.get("file")); + } + } + return 0; + } + + private int printEmptyJson(PrintWriter out, PrintWriter err) { + try { + Map result = new LinkedHashMap<>(); + result.put("keystores", new ArrayList<>()); + out.println(MAPPER.writeValueAsString(result)); + return 0; + } catch (Exception e) { + err.println("Error writing JSON output"); + return 1; + } + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java new file mode 100644 index 00000000000..39d2bdd3502 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java @@ -0,0 +1,77 @@ +package org.tron.plugins; + +import java.io.File; +import java.io.PrintWriter; +import java.util.concurrent.Callable; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "new", + mixinStandardHelpOptions = true, + description = "Generate a new keystore file with a random keypair.") +public class KeystoreNew implements Callable { + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--password-file"}, + description = "Read password from file instead of interactive prompt") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + try { + KeystoreCliUtils.ensureDirectory(keystoreDir); + + String password = KeystoreCliUtils.readPassword(passwordFile, err); + if (password == null) { + return 1; + } + + boolean ecKey = !sm2; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), ecKey); + String fileName = WalletUtils.generateWalletFile( + password, keyPair, keystoreDir, true); + + String address = Credentials.create(keyPair).getAddress(); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", address, "file", fileName)); + } else { + out.println("Your new key was generated"); + KeystoreCliUtils.printSecurityTips(out, address, + new File(keystoreDir, fileName).getPath()); + } + return 0; + } catch (CipherException e) { + err.println("Encryption error: " + e.getMessage()); + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java new file mode 100644 index 00000000000..4ef6cbbd71e --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java @@ -0,0 +1,245 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.tron.common.crypto.SignInterface; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Wallet; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; + +@Command(name = "update", + mixinStandardHelpOptions = true, + description = "Change the password of a keystore file.") +public class KeystoreUpdate implements Callable { + + private static final ObjectMapper MAPPER = KeystoreCliUtils.mapper(); + private static final String INPUT_CANCELLED = "Password input cancelled."; + + @Spec + private CommandSpec spec; + + @Parameters(index = "0", description = "Address of the keystore to update") + private String address; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--password-file"}, + description = "Read old and new passwords from file (one per line)") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + // Hoisted out of the try so the legacy-truncation hint in the catch + // block can inspect whether the user-supplied password contained + // whitespace (which is the only case truncation can explain). + String oldPassword = null; + try { + File keystoreFile = findKeystoreByAddress(address, err); + if (keystoreFile == null) { + // findKeystoreByAddress already prints the specific error + return 1; + } + + String newPassword; + + if (passwordFile != null) { + byte[] bytes = KeystoreCliUtils.readRegularFile( + passwordFile, 1024, "Password file", err); + if (bytes == null) { + return 1; + } + try { + String content = new String(bytes, StandardCharsets.UTF_8); + // Strip UTF-8 BOM if present (Windows Notepad) + if (content.length() > 0 && content.charAt(0) == '\uFEFF') { + content = content.substring(1); + } + // String.split with the default zero-limit form already drops + // trailing empty strings, so "old\nnew" and "old\nnew\n" both + // yield length 2; require strict equality so a stray third line + // (e.g. someone confusingly providing a confirm line, or the + // wrong file altogether) is reported rather than silently + // discarded. + String[] lines = content.split("\\r?\\n|\\r"); + if (lines.length != 2) { + err.println("Password file must contain exactly two lines: " + + "current password on the first line and new password " + + "on the second line (no confirmation line)."); + return 1; + } + oldPassword = WalletUtils.stripPasswordLine(lines[0]); + newPassword = WalletUtils.stripPasswordLine(lines[1]); + } finally { + Arrays.fill(bytes, (byte) 0); + } + } else { + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --password-file to provide passwords."); + return 1; + } + char[] oldPwd = console.readPassword("Enter current password: "); + if (oldPwd == null) { + err.println(INPUT_CANCELLED); + return 1; + } + char[] newPwd = console.readPassword("Enter new password: "); + if (newPwd == null) { + Arrays.fill(oldPwd, '\0'); + err.println(INPUT_CANCELLED); + return 1; + } + char[] confirmPwd = console.readPassword("Confirm new password: "); + if (confirmPwd == null) { + Arrays.fill(oldPwd, '\0'); + Arrays.fill(newPwd, '\0'); + err.println(INPUT_CANCELLED); + return 1; + } + try { + oldPassword = new String(oldPwd); + newPassword = new String(newPwd); + String confirmPassword = new String(confirmPwd); + if (!newPassword.equals(confirmPassword)) { + err.println("New passwords do not match."); + return 1; + } + } finally { + Arrays.fill(oldPwd, '\0'); + Arrays.fill(newPwd, '\0'); + Arrays.fill(confirmPwd, '\0'); + } + } + + // Skip validation on old password: keystore may predate the minimum-length policy + if (!WalletUtils.passwordValid(newPassword)) { + err.println("Invalid new password: must be at least 6 characters."); + return 1; + } + + boolean ecKey = !sm2; + // Re-read via NOFOLLOW byte channel to close the TOCTOU window between + // findKeystoreByAddress and this read — an attacker with directory + // write access could otherwise swap the file for a symlink in between. + byte[] keystoreBytes = KeystoreCliUtils.readKeystoreFile(keystoreFile, err); + if (keystoreBytes == null) { + // readKeystoreFile already printed the specific reason + return 1; + } + WalletFile walletFile = MAPPER.readValue(keystoreBytes, WalletFile.class); + SignInterface keyPair = Wallet.decrypt(oldPassword, walletFile, ecKey); + + // createStandard already sets the correctly-derived address. Do NOT override + // with walletFile.getAddress() — that would propagate a potentially spoofed + // address from the JSON. + WalletFile newWalletFile = Wallet.createStandard(newPassword, keyPair); + // writeWalletFile does a secure temp-file + atomic rename internally. + WalletUtils.writeWalletFile(newWalletFile, keystoreFile); + + // Use the derived address from newWalletFile, not walletFile.getAddress(). + // Defense-in-depth: Wallet.decrypt already rejects spoofed addresses, but + // relying on the derived value keeps this code correct even if that check + // is ever weakened. + String verifiedAddress = newWalletFile.getAddress(); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", verifiedAddress, + "file", keystoreFile.getName(), + "status", "updated")); + } else { + out.println("Password updated for: " + verifiedAddress); + } + return 0; + } catch (CipherException e) { + err.println("Decryption failed: " + e.getMessage()); + // Legacy-truncation hint: keystores created via + // `FullNode.jar --keystore-factory` in non-TTY mode (e.g. + // `echo PASS | java ...`) were encrypted with only the first + // whitespace-separated word of the password due to a bug in the + // legacy input path. The hint only fires if the provided password + // actually contains whitespace — otherwise truncation cannot be the + // cause of the decryption failure and the hint would be noise for + // the far more common "wrong password" case. + if (oldPassword != null && oldPassword.matches(".*\\s.*")) { + err.println("Tip: if this keystore was created with " + + "`FullNode.jar --keystore-factory` in non-TTY mode, the legacy " + + "code truncated the password at the first whitespace. " + + "Try re-running with only the first whitespace-separated word " + + "of your passphrase as the current password; you can then " + + "choose the full phrase as the new password."); + } + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } + + private File findKeystoreByAddress(String targetAddress, PrintWriter err) { + if (!keystoreDir.exists() || !keystoreDir.isDirectory()) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + File[] files = keystoreDir.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + java.util.List matches = new java.util.ArrayList<>(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile wf = MAPPER.readValue(bytes, WalletFile.class); + if (KeystoreCliUtils.isValidKeystoreFile(wf) + && targetAddress.equals(wf.getAddress())) { + matches.add(file); + } + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + if (matches.size() > 1) { + err.println("Multiple keystores found for address " + + targetAddress + ":"); + for (File m : matches) { + err.println(" " + m.getName()); + } + err.println("Please remove duplicates and retry."); + return null; + } + if (matches.isEmpty()) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + return matches.get(0); + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/Toolkit.java b/plugins/src/main/java/common/org/tron/plugins/Toolkit.java index 3b9972de1c5..7a979fe256c 100644 --- a/plugins/src/main/java/common/org/tron/plugins/Toolkit.java +++ b/plugins/src/main/java/common/org/tron/plugins/Toolkit.java @@ -3,7 +3,7 @@ import java.util.concurrent.Callable; import picocli.CommandLine; -@CommandLine.Command(subcommands = { CommandLine.HelpCommand.class, Db.class}) +@CommandLine.Command(subcommands = { CommandLine.HelpCommand.class, Db.class, Keystore.class}) public class Toolkit implements Callable { diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java new file mode 100644 index 00000000000..264e1cb4519 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java @@ -0,0 +1,348 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.keystore.WalletFile; + +public class KeystoreCliUtilsTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testJsonMapEven() { + Map m = KeystoreCliUtils.jsonMap("a", "1", "b", "2"); + assertEquals(2, m.size()); + assertEquals("1", m.get("a")); + assertEquals("2", m.get("b")); + } + + @Test + public void testJsonMapPreservesOrder() { + Map m = KeystoreCliUtils.jsonMap( + "z", "1", "a", "2", "m", "3"); + String[] keys = m.keySet().toArray(new String[0]); + assertEquals("z", keys[0]); + assertEquals("a", keys[1]); + assertEquals("m", keys[2]); + } + + @Test + public void testJsonMapEmpty() { + Map m = KeystoreCliUtils.jsonMap(); + assertTrue(m.isEmpty()); + } + + private static WalletFile.Crypto supportedCrypto() { + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("scrypt"); + return crypto; + } + + @Test + public void testIsValidKeystoreFileValid() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + wf.setCrypto(supportedCrypto()); + assertTrue(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileNullAddress() { + WalletFile wf = new WalletFile(); + wf.setVersion(3); + wf.setCrypto(supportedCrypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileNullCrypto() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileWrongVersion() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(2); + wf.setCrypto(supportedCrypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsEmptyCryptoStub() { + // {"address":"T...","version":3,"crypto":{}} — passes the old checks + // but Wallet.validate would later reject it. Discovery should skip it. + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + wf.setCrypto(new WalletFile.Crypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsUnsupportedCipher() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("des"); + crypto.setKdf("scrypt"); + wf.setCrypto(crypto); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsUnsupportedKdf() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("bcrypt"); + wf.setCrypto(crypto); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileAcceptsPbkdf2Kdf() { + // pbkdf2 is the other supported KDF (used by some Ethereum keystores). + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("pbkdf2"); + wf.setCrypto(crypto); + assertTrue(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testCheckFileExistsNull() { + StringWriter err = new StringWriter(); + assertTrue(KeystoreCliUtils.checkFileExists(null, "Label", + new PrintWriter(err))); + assertEquals("", err.toString()); + } + + @Test + public void testCheckFileExistsMissing() { + StringWriter err = new StringWriter(); + File missing = new File("/tmp/nonexistent-cli-utils-test-file"); + assertFalse(KeystoreCliUtils.checkFileExists(missing, "Key file", + new PrintWriter(err))); + assertTrue(err.toString().contains("Key file not found")); + } + + @Test + public void testCheckFileExistsPresent() throws Exception { + StringWriter err = new StringWriter(); + File f = tempFolder.newFile("present.txt"); + assertTrue(KeystoreCliUtils.checkFileExists(f, "Key file", + new PrintWriter(err))); + } + + @Test + public void testReadPasswordFromFile() throws Exception { + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "goodpassword".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFromFileWithLineEndings() throws Exception { + File pwFile = tempFolder.newFile("pw-crlf.txt"); + Files.write(pwFile.toPath(), "goodpassword\r\n".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFromFileWithBom() throws Exception { + File pwFile = tempFolder.newFile("pw-bom.txt"); + Files.write(pwFile.toPath(), + "\uFEFFgoodpassword".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFileTooLarge() throws Exception { + File pwFile = tempFolder.newFile("pw-big.txt"); + byte[] big = new byte[1025]; + java.util.Arrays.fill(big, (byte) 'a'); + Files.write(pwFile.toPath(), big); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("too large")); + } + + @Test + public void testReadPasswordFileShort() throws Exception { + File pwFile = tempFolder.newFile("pw-short.txt"); + Files.write(pwFile.toPath(), "abc".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("at least 6")); + } + + @Test + public void testReadPasswordFileNotFound() throws Exception { + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword( + new File("/tmp/nonexistent-pw-direct-test.txt"), new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("Password file not found")); + } + + @Test + public void testEnsureDirectoryCreatesNested() throws Exception { + File dir = new File(tempFolder.getRoot(), "a/b/c"); + assertFalse(dir.exists()); + KeystoreCliUtils.ensureDirectory(dir); + assertTrue(dir.exists()); + assertTrue(dir.isDirectory()); + } + + @Test + public void testEnsureDirectoryExisting() throws Exception { + File dir = tempFolder.newFolder("existing"); + KeystoreCliUtils.ensureDirectory(dir); + assertTrue(dir.isDirectory()); + } + + @Test(expected = java.io.IOException.class) + public void testEnsureDirectoryPathIsFile() throws Exception { + File f = tempFolder.newFile("not-a-dir"); + KeystoreCliUtils.ensureDirectory(f); + } + + @Test + public void testPrintJsonValidOutput() { + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + KeystoreCliUtils.printJson(new PrintWriter(out), new PrintWriter(err), + KeystoreCliUtils.jsonMap("address", "TAddr", "file", "file.json")); + String s = out.toString().trim(); + assertTrue(s.contains("\"address\":\"TAddr\"")); + assertTrue(s.contains("\"file\":\"file.json\"")); + } + + @Test + public void testPrintSecurityTipsIncludesAddressAndFile() { + StringWriter out = new StringWriter(); + KeystoreCliUtils.printSecurityTips(new PrintWriter(out), + "TMyAddress", "/path/to/keystore.json"); + String s = out.toString(); + assertTrue(s.contains("TMyAddress")); + assertTrue(s.contains("/path/to/keystore.json")); + assertTrue(s.contains("NEVER share")); + assertTrue(s.contains("BACKUP")); + assertTrue(s.contains("REMEMBER")); + } + + @Test + public void testReadRegularFileSuccess() throws Exception { + File f = tempFolder.newFile("regular.txt"); + Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", + new PrintWriter(err)); + assertNotNull(bytes); + assertEquals("hello", new String(bytes, StandardCharsets.UTF_8)); + } + + @Test + public void testReadRegularFileMissing() throws Exception { + File f = new File(tempFolder.getRoot(), "does-not-exist"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "Password file", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected 'not found' error, got: " + err.toString(), + err.toString().contains("Password file not found")); + } + + @Test + public void testReadRegularFileTooLarge() throws Exception { + File f = tempFolder.newFile("big.txt"); + byte[] big = new byte[2048]; + java.util.Arrays.fill(big, (byte) 'a'); + Files.write(f.toPath(), big); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "Password file", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected 'too large', got: " + err.toString(), + err.toString().contains("too large")); + } + + @Test + public void testReadRegularFileRefusesSymlink() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File target = tempFolder.newFile("real-target.txt"); + Files.write(target.toPath(), "secret content".getBytes(StandardCharsets.UTF_8)); + File link = new File(tempFolder.getRoot(), "symlink.txt"); + Files.createSymbolicLink(link.toPath(), target.toPath()); + + StringWriter err = new StringWriter(); + byte[] bytes = KeystoreCliUtils.readRegularFile(link, 1024, "File", + new PrintWriter(err)); + + assertNull("Must refuse to read through symlink", bytes); + assertTrue("Expected symlink-refusal message, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testReadRegularFileRefusesDirectory() throws Exception { + File dir = tempFolder.newFolder("a-dir"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(dir, 1024, "File", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected not-regular-file error, got: " + err.toString(), + err.toString().contains("Not a regular file")); + } + + @Test + public void testReadRegularFileEmptyFile() throws Exception { + File f = tempFolder.newFile("empty.txt"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", + new PrintWriter(err)); + assertNotNull(bytes); + assertEquals(0, bytes.length); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java new file mode 100644 index 00000000000..3e718dfd143 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java @@ -0,0 +1,536 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreImportTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testImportWithKeyFileAndPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore"); + + // Generate a known private key + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String expectedAddress = Credentials.create(keyPair).getAddress(); + + File keyFile = tempFolder.newFile("private.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("password.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify roundtrip: decrypt should recover the same private key + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertEquals("Address must match", expectedAddress, creds.getAddress()); + assertArrayEquals("Private key must survive import roundtrip", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportInvalidKeyTooShort() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + File keyFile = tempFolder.newFile("bad.key"); + Files.write(keyFile.toPath(), "abcdef1234".getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with invalid key", 1, exitCode); + } + + @Test + public void testImportInvalidKeyNonHex() throws Exception { + File dir = tempFolder.newFolder("keystore-hex"); + File keyFile = tempFolder.newFile("nonhex.key"); + Files.write(keyFile.toPath(), + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + .getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with non-hex key", 1, exitCode); + } + + @Test + public void testImportNoTtyNoKeyFile() throws Exception { + File dir = tempFolder.newFolder("keystore-notty"); + File pwFile = tempFolder.newFile("pw2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // No --key-file and System.console() is null in CI + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --key-file", 1, exitCode); + } + + @Test + public void testImportWithSm2() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + // SM2 uses same 32-byte private key format + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), false); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("sm2.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 import should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify SM2 keystore can be decrypted + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], false); + assertArrayEquals("SM2 key must survive import roundtrip", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportKeyFileWithWhitespace() throws Exception { + File dir = tempFolder.newFolder("keystore-ws"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + // Key file with leading/trailing whitespace and newlines + File keyFile = tempFolder.newFile("ws.key"); + Files.write(keyFile.toPath(), + (" " + privateKeyHex + " \n\n").getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-ws.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with whitespace-padded key should succeed", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertArrayEquals("Key must survive whitespace-trimmed import", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportDuplicateAddressBlocked() throws Exception { + File dir = tempFolder.newFolder("keystore-dup"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("dup.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-dup.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // First import succeeds + CommandLine cmd1 = new CommandLine(new Toolkit()); + assertEquals(0, cmd1.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + + // Second import of same key is blocked + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd2 = new CommandLine(new Toolkit()); + cmd2.setErr(new java.io.PrintWriter(err)); + assertEquals("Duplicate import should be blocked", 1, + cmd2.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + assertTrue("Error should mention already exists", + err.toString().contains("already exists")); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Should still have only 1 keystore", 1, files.length); + } + + @Test + public void testImportDuplicateAddressWithForce() throws Exception { + File dir = tempFolder.newFolder("keystore-force"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("force.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-force.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // First import + CommandLine cmd1 = new CommandLine(new Toolkit()); + assertEquals(0, cmd1.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + + // Second import with --force succeeds + CommandLine cmd2 = new CommandLine(new Toolkit()); + assertEquals(0, cmd2.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--force")); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Force import should create 2 files", 2, files.length); + } + + @Test + public void testImportKeyFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nokey"); + File pwFile = tempFolder.newFile("pw-nokey.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", "/tmp/nonexistent-key-file.txt", + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when key file not found", 1, exitCode); + } + + @Test + public void testImportWith0xPrefix() throws Exception { + File dir = tempFolder.newFolder("keystore-0x"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String expectedAddress = Credentials.create(keyPair).getAddress(); + + File keyFile = tempFolder.newFile("0x.key"); + Files.write(keyFile.toPath(), + ("0x" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-0x.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with 0x prefix should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertEquals("Address must match", expectedAddress, creds.getAddress()); + } + + @Test + public void testImportWith0XUppercasePrefix() throws Exception { + File dir = tempFolder.newFolder("keystore-0X"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("0X.key"); + Files.write(keyFile.toPath(), + ("0X" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-0X.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with 0X prefix should succeed", 0, exitCode); + } + + @Test + public void testImportWarnsOnCorruptedFile() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + // Create a corrupted JSON in the keystore dir + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + File keyFile = tempFolder.newFile("warn.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-warn.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter out = new java.io.StringWriter(); + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new java.io.PrintWriter(out)); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + String errOutput = err.toString(); + assertTrue("Should warn about corrupted file", + errOutput.contains("Warning: skipping unreadable file: corrupted.json")); + } + + @Test + public void testImportKeystoreFilePermissions() throws Exception { + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("perm.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-perm.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + java.util.Set perms = + Files.getPosixFilePermissions(files[0].toPath()); + assertEquals("Keystore file should have owner-only permissions (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testImportRefusesSymlinkKeyFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-symlink"); + // Create a real key file and a symlink pointing to it + File target = tempFolder.newFile("real.key"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + Files.write(target.toPath(), + ByteArray.toHexString(keyPair.getPrivateKey()).getBytes(StandardCharsets.UTF_8)); + + File symlink = new File(tempFolder.getRoot(), "symlink.key"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File pwFile = tempFolder.newFile("pw-symlink.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", symlink.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Must refuse symlinked key file", 1, exitCode); + assertTrue("Expected symlink-refusal error, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testImportRefusesSymlinkPasswordFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-pwsymlink"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + File keyFile = tempFolder.newFile("sym-pw.key"); + Files.write(keyFile.toPath(), + ByteArray.toHexString(keyPair.getPrivateKey()).getBytes(StandardCharsets.UTF_8)); + + File realPwFile = tempFolder.newFile("real-pw.txt"); + Files.write(realPwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + File pwSymlink = new File(tempFolder.getRoot(), "pw-symlink.txt"); + Files.createSymbolicLink(pwSymlink.toPath(), realPwFile.toPath()); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwSymlink.getAbsolutePath()); + + assertEquals("Must refuse symlinked password file", 1, exitCode); + assertTrue("Expected symlink-refusal error, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testImportDuplicateCheckSkipsInvalidVersion() throws Exception { + File dir = tempFolder.newFolder("keystore-badver"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String address = Credentials.create(keyPair).getAddress(); + + // Create a JSON with correct address but wrong version — should NOT count as duplicate + String fakeKeystore = "{\"address\":\"" + address + + "\",\"version\":2,\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "fake.json").toPath(), + fakeKeystore.getBytes(StandardCharsets.UTF_8)); + + File keyFile = tempFolder.newFile("ver.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-ver.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import should succeed — invalid-version file is not a real duplicate", 0, + exitCode); + } + + @Test + public void testImportDuplicateScanSkipsSymlinkedEntry() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-dup-symlink"); + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File keyFile = tempFolder.newFile("dup-sym.key"); + Files.write(keyFile.toPath(), + privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-dup-sym.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import should succeed with symlink present", 0, exitCode); + assertTrue("Duplicate scan must warn about the symlinked entry, got: " + + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + } + + @Test + public void testImportRejectsMultiLinePasswordFile() throws Exception { + // Regression: a user might accidentally point --password-file at a + // `keystore update` two-line file; without the guard that literal + // "old\nnew" becomes the password. + File dir = tempFolder.newFolder("keystore-multi-pw"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("multi.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("multi-pw.txt"); + Files.write(pwFile.toPath(), + "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should reject multi-line password file", 1, exitCode); + assertTrue("Error must explain the multi-line rejection, got: " + err.toString(), + err.toString().contains("multiple lines")); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertTrue("No keystore should have been created", + files == null || files.length == 0); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java new file mode 100644 index 00000000000..b029ddaf9f7 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java @@ -0,0 +1,282 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreListTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testListMultipleKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore"); + String password = "test123456"; + + // Create 3 keystores + for (int i = 0; i < 3; i++) { + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + } + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Output should not be empty", output.length() > 0); + // Should have 3 lines of output (one per keystore) + String[] lines = output.split("\\n"); + assertEquals("Should list 3 keystores", 3, lines.length); + // Each line should contain a T-address and a .json filename + for (String line : lines) { + assertTrue("Each line should contain an address starting with T", + line.trim().startsWith("T")); + assertTrue("Each line should reference a .json file", + line.contains(".json")); + } + } + + @Test + public void testListEmptyDirectory() throws Exception { + File dir = tempFolder.newFolder("empty"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should print no-keystores message", + out.toString().contains("No keystores found")); + } + + @Test + public void testListNonExistentDirectory() throws Exception { + File dir = new File(tempFolder.getRoot(), "nonexistent"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should print no-keystores message", + out.toString().contains("No keystores found")); + } + + @Test + public void testListEmptyDirectoryJsonOutput() throws Exception { + File dir = tempFolder.newFolder("empty-json"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Empty dir JSON should have empty keystores array", + output.contains("{\"keystores\":[]}")); + } + + @Test + public void testListNonExistentDirectoryJsonOutput() throws Exception { + File dir = new File(tempFolder.getRoot(), "nonexistent-json"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Non-existent dir JSON should have empty keystores array", + output.contains("{\"keystores\":[]}")); + } + + @Test + public void testListJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + String password = "test123456"; + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Should start with keystores JSON array", + output.startsWith("{\"keystores\":[")); + assertTrue("Should end with JSON array close", + output.endsWith("]}")); + } + + @Test + public void testListSkipsNonKeystoreFiles() throws Exception { + File dir = tempFolder.newFolder("keystore-mixed"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create non-keystore files + Files.write(new File(dir, "readme.json").toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + Files.write(new File(dir, "notes.txt").toPath(), + "plain text".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Output should not be empty", output.length() > 0); + String[] lines = output.split("\\n"); + // Should list only the valid keystore, not the readme.json or notes.txt + assertEquals("Should list only 1 valid keystore", 1, lines.length); + } + + @Test + public void testListWarnsOnCorruptedJsonFiles() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create a corrupted JSON file + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String errOutput = err.toString(); + assertTrue("Should warn about corrupted file", + errOutput.contains("Warning: skipping unreadable file: corrupted.json")); + + // Valid keystore should still be listed + String output = out.toString().trim(); + assertTrue("Should still list the valid keystore", output.length() > 0); + } + + @Test + public void testListSkipsInvalidVersionKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore-version"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create a JSON with address and crypto but wrong version + String fakeV2 = "{\"address\":\"TFakeAddress\",\"version\":2," + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "v2-keystore.json").toPath(), + fakeV2.getBytes(StandardCharsets.UTF_8)); + + // Create a JSON with address but null crypto + String noCrypto = "{\"address\":\"TFakeAddress2\",\"version\":3}"; + Files.write(new File(dir, "no-crypto.json").toPath(), + noCrypto.getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + String[] lines = output.split("\\n"); + assertEquals("Should list only the valid v3 keystore, not v2 or no-crypto", + 1, lines.length); + } + + @Test + public void testListSkipsSymlinkedKeystoreFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-symlink-scan"); + String password = "test123456"; + + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // A JSON file elsewhere (simulates "target we should not be tricked + // into reading") — placed outside the keystore dir. + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"secret\":\"should not appear in list output\"}" + .getBytes(StandardCharsets.UTF_8)); + + // Plant a .json symlink in the keystore dir + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should warn about symbolic link, got err: " + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + String output = out.toString().trim(); + String[] lines = output.split("\\n"); + assertEquals("Should list only the real keystore", 1, lines.length); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java new file mode 100644 index 00000000000..0819103642e --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java @@ -0,0 +1,307 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreNewTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testNewKeystoreWithPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore"); + File pwFile = tempFolder.newFile("password.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Should create exactly one keystore file", 1, files.length); + + // Verify the file is a valid keystore + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertNotNull(creds.getAddress()); + assertTrue(creds.getAddress().startsWith("T")); + } + + @Test + public void testNewKeystoreJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + File pwFile = tempFolder.newFile("password-json.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("JSON output should contain address", + output.contains("\"address\"")); + assertTrue("JSON output should contain file", + output.contains("\"file\"")); + } + + @Test + public void testNewKeystoreInvalidPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + File pwFile = tempFolder.newFile("short.txt"); + Files.write(pwFile.toPath(), "abc".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with short password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testNewKeystoreCustomDir() throws Exception { + File dir = new File(tempFolder.getRoot(), "custom/nested/dir"); + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Custom dir should be created", dir.exists()); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + } + + @Test + public void testNewKeystoreNoTtyNoPasswordFile() throws Exception { + // In CI/test environment, System.console() is null. + // Without --password-file, should fail with exit code 1. + File dir = tempFolder.newFolder("keystore-notty"); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --password-file", 1, exitCode); + } + + @Test + public void testNewKeystoreEmptyPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-empty"); + File pwFile = tempFolder.newFile("empty.txt"); + Files.write(pwFile.toPath(), "".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with empty password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testNewKeystoreWithSm2() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 keystore creation should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify SM2 keystore can be decrypted with ecKey=false + org.tron.keystore.Credentials creds = + org.tron.keystore.WalletUtils.loadCredentials("test123456", files[0], false); + assertNotNull(creds.getAddress()); + } + + @Test + public void testNewKeystoreSpecialCharPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-special"); + File pwFile = tempFolder.newFile("pw-special.txt"); + String password = "p@$$w0rd!#%^&*()_+-=[]{}"; + Files.write(pwFile.toPath(), password.getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify can decrypt with same special-char password + Credentials creds = WalletUtils.loadCredentials(password, files[0], true); + assertNotNull(creds.getAddress()); + } + + @Test + public void testNewKeystorePasswordFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nopw"); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", "/tmp/nonexistent-pw.txt"); + + assertEquals("Should fail when password file not found", 1, exitCode); + } + + @Test + public void testNewKeystoreDirIsFile() throws Exception { + File notADir = tempFolder.newFile("not-a-dir"); + File pwFile = tempFolder.newFile("pw-dir.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", notADir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when dir is a file", 1, exitCode); + } + + @Test + public void testNewKeystorePasswordFileTooLarge() throws Exception { + File dir = tempFolder.newFolder("keystore-bigpw"); + File pwFile = tempFolder.newFile("bigpw.txt"); + byte[] bigContent = new byte[1025]; + java.util.Arrays.fill(bigContent, (byte) 'a'); + Files.write(pwFile.toPath(), bigContent); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with large password file", 1, exitCode); + assertTrue("Error should mention file too large", + err.toString().contains("too large")); + } + + @Test + public void testNewKeystorePasswordFileWithBom() throws Exception { + File dir = tempFolder.newFolder("keystore-bom"); + File pwFile = tempFolder.newFile("bom.txt"); + Files.write(pwFile.toPath(), + ("\uFEFF" + "test123456").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should succeed with BOM password file", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + } + + @Test + public void testNewKeystoreFilePermissions() throws Exception { + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + File pwFile = tempFolder.newFile("pw-perms.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + java.util.Set perms = + Files.getPosixFilePermissions(files[0].toPath()); + assertEquals("Keystore file should have owner-only permissions (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testNewKeystoreRejectsMultiLinePasswordFile() throws Exception { + // Regression: a user might accidentally point --password-file at a + // `keystore update` two-line file (old\nnew). Without the guard the + // literal "old\nnew" becomes the password and neither line alone can + // unlock it later. new/import must reject multi-line files. + File dir = tempFolder.newFolder("keystore-multi"); + File pwFile = tempFolder.newFile("multi-line.txt"); + Files.write(pwFile.toPath(), + "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should reject multi-line password file", 1, exitCode); + assertTrue("Error must explain the multi-line rejection, got: " + err.toString(), + err.toString().contains("multiple lines")); + // No keystore created + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertTrue("No keystore should have been created", + files == null || files.length == 0); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java new file mode 100644 index 00000000000..cdbe9b3816b --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java @@ -0,0 +1,822 @@ +package org.tron.plugins; + +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 com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreUpdateTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void testUpdatePassword() throws Exception { + File dir = tempFolder.newFolder("keystore"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + String address = creds.getAddress(); + + File pwFile = tempFolder.newFile("passwords.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + // Verify: new password works and key survives + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive password change", + originalKey, updated.getSignInterface().getPrivateKey()); + + // Verify: address field preserved in keystore JSON + WalletFile wf = MAPPER.readValue(new File(dir, fileName), WalletFile.class); + assertEquals("Address must be preserved in updated keystore", + address, wf.getAddress()); + } + + @Test + public void testUpdateWrongOldPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + String password = "correct123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + String address = creds.getAddress(); + + File pwFile = tempFolder.newFile("wrong.txt"); + Files.write(pwFile.toPath(), + ("wrongpass1\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with wrong password", 1, exitCode); + assertTrue("Error should mention decryption", + err.toString().contains("Decryption failed")); + + // Verify: original password still works (file unchanged) + Credentials unchanged = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + assertEquals(address, unchanged.getAddress()); + } + + @Test + public void testUpdateNonExistentAddress() throws Exception { + File dir = tempFolder.newFolder("keystore-noaddr"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, keyPair, dir, true); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), + ("test123456\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TNonExistentAddress123456789", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail for non-existent address", 1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateNewPasswordTooShort() throws Exception { + File dir = tempFolder.newFolder("keystore-shortpw"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("shortpw.txt"); + Files.write(pwFile.toPath(), + (password + "\nabc").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with short new password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testUpdateWithWindowsLineEndings() throws Exception { + File dir = tempFolder.newFolder("keystore-crlf"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("crlf.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\r\n" + newPassword + "\r\n").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with CRLF password file should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with CRLF passwords", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("pw-json.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("JSON should contain address", + output.contains("\"address\"")); + assertTrue("JSON should contain status updated", + output.contains("\"updated\"")); + assertTrue("JSON should contain file", + output.contains("\"file\"")); + } + + @Test + public void testUpdateWarnsOnCorruptedFile() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw-corrupt.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should warn about corrupted file", + err.toString().contains("Warning: skipping unreadable file: corrupted.json")); + } + + @Test + public void testUpdatePasswordFileOnlyOneLine() throws Exception { + File dir = tempFolder.newFolder("keystore-1line"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("oneline.txt"); + Files.write(pwFile.toPath(), + "onlyoldpassword".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with single-line password file", 1, exitCode); + assertTrue("Error should mention exactly two lines", + err.toString().contains("exactly two lines")); + } + + @Test + public void testUpdatePasswordFileThreeLines() throws Exception { + // Regression: a three-line password file (e.g. someone confusingly added a + // confirm line, or pointed at the wrong file) must be rejected, not have + // the third line silently discarded. The original keystore must remain + // decryptable with the old password. + File dir = tempFolder.newFolder("keystore-3line"); + String oldPassword = "oldpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Snapshot the keystore bytes so we can verify the file is untouched. + byte[] beforeBytes = Files.readAllBytes(new File(dir, fileName).toPath()); + + File pwFile = tempFolder.newFile("threeline.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\nnewpass456\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with three-line password file", 1, exitCode); + assertTrue("Error should mention exactly two lines, got: " + err.toString(), + err.toString().contains("exactly two lines")); + + // Verify: keystore file is byte-for-byte unchanged + byte[] afterBytes = Files.readAllBytes(new File(dir, fileName).toPath()); + assertArrayEquals("Keystore file must not be modified on rejection", + beforeBytes, afterBytes); + + // Verify: original password still decrypts the keystore + Credentials unchanged = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + assertArrayEquals("Original key must still be recoverable with old password", + originalKey, unchanged.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateNoTtyNoPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore-notty"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --password-file", 1, exitCode); + assertTrue("Error should mention no terminal", + err.toString().contains("No interactive terminal")); + } + + @Test + public void testUpdatePasswordFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nopwf"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", "/tmp/nonexistent-pw-update.txt"); + + assertEquals("Should fail when password file not found", 1, exitCode); + assertTrue("Error should mention file not found", + err.toString().contains("Password file not found")); + } + + @Test + public void testUpdateSm2Keystore() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), false); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), false); + + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 keystore update should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), false); + assertArrayEquals("SM2 key must survive password change", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateMultipleKeystoresSameAddress() throws Exception { + File dir = tempFolder.newFolder("keystore-multi"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String address = Credentials.create(keyPair).getAddress(); + + // Create two keystores for the same address via direct API + WalletUtils.generateWalletFile(password, keyPair, dir, true); + // Small delay to get different filename timestamps + Thread.sleep(50); + WalletUtils.generateWalletFile(password, keyPair, dir, true); + + File pwFile = tempFolder.newFile("pw-multi.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with multiple keystores for same address", 1, exitCode); + assertTrue("Error should mention multiple keystores", + err.toString().contains("Multiple keystores found")); + assertTrue("Error should mention remove duplicates", + err.toString().contains("remove duplicates")); + } + + @Test + public void testUpdatePasswordFileTooLarge() throws Exception { + File dir = tempFolder.newFolder("keystore-bigpw"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + // Create a password file > 1KB + File pwFile = tempFolder.newFile("bigpw.txt"); + byte[] bigContent = new byte[1025]; + java.util.Arrays.fill(bigContent, (byte) 'a'); + Files.write(pwFile.toPath(), bigContent); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with large password file", 1, exitCode); + assertTrue("Error should mention file too large", + err.toString().contains("too large")); + } + + @Test + public void testUpdatePasswordFileWithBom() throws Exception { + File dir = tempFolder.newFolder("keystore-bom"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Password file with UTF-8 BOM + File pwFile = tempFolder.newFile("bom.txt"); + Files.write(pwFile.toPath(), + ("\uFEFF" + oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with BOM password file should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with BOM password file", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateNonExistentKeystoreDir() throws Exception { + File dir = new File(tempFolder.getRoot(), "does-not-exist"); + + File pwFile = tempFolder.newFile("pw-nodir.txt"); + Files.write(pwFile.toPath(), + ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TSomeAddress", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateKeystoreDirIsFile() throws Exception { + File notADir = tempFolder.newFile("not-a-dir"); + + File pwFile = tempFolder.newFile("pw-notdir.txt"); + Files.write(pwFile.toPath(), + ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TSomeAddress", + "--keystore-dir", notADir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateWithOldMacLineEndings() throws Exception { + File dir = tempFolder.newFolder("keystore-cr"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Password file with old Mac line endings (\r only) + File pwFile = tempFolder.newFile("cr.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\r" + newPassword + "\r").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with old Mac CR line endings should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with CR passwords", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateSkipsInvalidVersionKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore-badver"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String address = Credentials.create(keyPair).getAddress(); + + // Create a JSON file with correct address but wrong version + String fakeKeystore = "{\"address\":\"" + address + + "\",\"version\":2,\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "fake.json").toPath(), + fakeKeystore.getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw-badver.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should not find keystore with wrong version", 1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found")); + } + + @Test + public void testUpdateRejectsTamperedAddressKeystore() throws Exception { + File dir = tempFolder.newFolder("keystore-tampered"); + String password = "test123456"; + + // Create a real keystore, then tamper with the address field to simulate + // a spoofed keystore that claims a different address than its encrypted key. + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + File keystoreFile = new File(dir, fileName); + + String realAddress = Credentials.create(keyPair).getAddress(); + String spoofedAddress = "TSpoofedAddressXXXXXXXXXXXXXXXXXXXX"; + + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper() + .configure(com.fasterxml.jackson.databind.DeserializationFeature + .FAIL_ON_UNKNOWN_PROPERTIES, false); + org.tron.keystore.WalletFile wf = mapper.readValue(keystoreFile, + org.tron.keystore.WalletFile.class); + wf.setAddress(spoofedAddress); + mapper.writeValue(keystoreFile, wf); + + File pwFile = tempFolder.newFile("pw-tampered.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", spoofedAddress, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail decryption on tampered address", 1, exitCode); + assertTrue("Error should mention address mismatch, got: " + err.toString(), + err.toString().contains("address mismatch")); + } + + @Test + public void testUpdatePreservesCorrectDerivedAddress() throws Exception { + // After update, the keystore's address field should be the derived address, + // not carried over from the original JSON (defense-in-depth against any + // residual spoofed address that somehow passed decryption). + File dir = tempFolder.newFolder("keystore-derived"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + String originalAddress = Credentials.create(keyPair).getAddress(); + + File pwFile = tempFolder.newFile("pw-derived.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", originalAddress, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + + // Verify updated file has the derived address + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + org.tron.keystore.WalletFile wf = mapper.readValue(new File(dir, fileName), + org.tron.keystore.WalletFile.class); + assertEquals("Updated keystore address must match derived address", + originalAddress, wf.getAddress()); + } + + @Test + public void testUpdateNarrowsLoosePermissionsTo0600() throws Exception { + // Adversarial test: pre-loosen the keystore to 0644, then verify that + // update writes the file back with 0600. This exercises the temp-file + // + atomic-rename path rather than merely preserving existing perms. + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Deliberately loosen to 0644 before update + java.nio.file.Path keystorePath = new File(dir, fileName).toPath(); + java.nio.file.Files.setPosixFilePermissions(keystorePath, + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE, + java.nio.file.attribute.PosixFilePermission.GROUP_READ, + java.nio.file.attribute.PosixFilePermission.OTHERS_READ)); + + File pwFile = tempFolder.newFile("pw-perms.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + + // Verify the updated keystore file is now owner-only (0600), not 0644 + java.util.Set perms = + java.nio.file.Files.getPosixFilePermissions(keystorePath); + assertEquals("Updated keystore must be narrowed to owner-only (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testUpdateLegacyTipFiresWhenPasswordHasWhitespace() throws Exception { + // The legacy-truncation tip should fire when the entered old password + // contains whitespace and decryption fails — the scenario that actually + // matches the legacy bug. + File dir = tempFolder.newFolder("keystore-tip-ws"); + String realPassword = "realpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(realPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(realPassword, + new File(dir, fileName), true); + + // Password with internal whitespace that is NOT the real password + File pwFile = tempFolder.newFile("pw-ws.txt"); + Files.write(pwFile.toPath(), + ("correct horse battery staple\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Legacy-truncation tip should fire for whitespace password, got: " + + err.toString(), + err.toString().contains("first whitespace-separated word")); + } + + @Test + public void testUpdateLegacyTipSuppressedWhenPasswordHasNoWhitespace() throws Exception { + // For the common "wrong password" case (no whitespace), the legacy tip + // would be noise — it should be suppressed. + File dir = tempFolder.newFolder("keystore-tip-nows"); + String realPassword = "realpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(realPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(realPassword, + new File(dir, fileName), true); + + // Wrong password with no whitespace + File pwFile = tempFolder.newFile("pw-nows.txt"); + Files.write(pwFile.toPath(), + ("wrongpassword\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Decryption failure must still be reported", + err.toString().contains("Decryption failed")); + assertFalse("Legacy-truncation tip should NOT fire for whitespace-free password", + err.toString().contains("first whitespace-separated word")); + } + + @Test + public void testUpdateScanSkipsSymlinkedEntry() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-update-symlink"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File pwFile = tempFolder.newFile("pw-update-sym.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update should succeed; symlinked entry must not break scan", 0, exitCode); + assertTrue("Scan must warn about the symlinked entry, got: " + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + } +} From 6f63a8c633cf3163c91bcbc5faa1cf649479ba4d Mon Sep 17 00:00:00 2001 From: Jeremy Zhang <50477615+warku123@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:38:41 +0800 Subject: [PATCH 27/43] fix(db): resolve resource leakage in CheckPointV2Store `close()` (#6688) --- .../java/org/tron/core/db/TronDatabase.java | 18 +- .../tron/core/store/CheckPointV2Store.java | 10 +- .../common/logsfilter/FilterQueryTest.java | 6 + .../tron/core/db/CheckPointV2StoreTest.java | 161 ++++++++++++++++++ .../org/tron/core/db/TronDatabaseTest.java | 52 ++++++ .../tron/core/event/BlockEventGetTest.java | 1 + 6 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java diff --git a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java index 40762568c82..237ab8dbaad 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java @@ -76,13 +76,25 @@ public void reset() { @Override public void close() { logger.info("******** Begin to close {}. ********", getName()); + doClose(); + logger.info("******** End to close {}. ********", getName()); + } + + /** + * Releases writeOptions and dbSource (best-effort, exceptions logged at WARN). + * Subclasses with extra resources should override {@link #close()} and call + * {@code doClose()} directly — not {@code super.close()} — to avoid duplicated logs. + */ + protected void doClose() { try { writeOptions.close(); + } catch (Exception e) { + logger.warn("Failed to close writeOptions in {}.", getName(), e); + } + try { dbSource.closeDB(); } catch (Exception e) { - logger.warn("Failed to close {}.", getName(), e); - } finally { - logger.info("******** End to close {}. ********", getName()); + logger.warn("Failed to close dbSource in {}.", getName(), e); } } diff --git a/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java b/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java index f027bd02664..fa8092273d2 100644 --- a/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java +++ b/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java @@ -62,20 +62,16 @@ public void updateByBatch(Map rows) { this.dbSource.updateByBatch(rows, writeOptions); } - /** - * close the database. - */ @Override public void close() { logger.debug("******** Begin to close {}. ********", getName()); try { writeOptions.close(); - dbSource.closeDB(); } catch (Exception e) { - logger.warn("Failed to close {}.", getName(), e); - } finally { - logger.debug("******** End to close {}. ********", getName()); + logger.warn("Failed to close writeOptions in {}.", getName(), e); } + doClose(); + logger.debug("******** End to close {}. ********", getName()); } } 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 5ee32d98ee6..c87d8e1136e 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.tron.common.logsfilter.capsule.ContractEventTriggerCapsule; @@ -28,6 +29,11 @@ @Slf4j public class FilterQueryTest { + @After + public void tearDown() { + EventPluginLoader.getInstance().setFilterQuery(null); + } + @Test public synchronized void testParseFilterQueryBlockNumber() { assertEquals(LATEST_BLOCK_NUM, parseToBlockNumber(EMPTY)); diff --git a/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java b/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java new file mode 100644 index 00000000000..bdb13376f34 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java @@ -0,0 +1,161 @@ +package org.tron.core.db; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.lang.reflect.Field; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.RocksDB; +import org.tron.common.TestConstants; +import org.tron.common.storage.WriteOptionsWrapper; +import org.tron.core.config.args.Args; +import org.tron.core.db.common.DbSourceInter; +import org.tron.core.store.CheckPointV2Store; + +public class CheckPointV2StoreTest { + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + static { + RocksDB.loadLibrary(); + } + + @BeforeClass + public static void initArgs() throws IOException { + Args.setParam( + new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF + ); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + + @Test + public void testStubMethods() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-stubs"); + try { + byte[] key = "key".getBytes(); + + store.put(key, new byte[]{}); + Assert.assertNull(store.get(key)); + Assert.assertFalse(store.has(key)); + store.forEach(item -> { + }); + Assert.assertNull(store.spliterator()); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = + (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + store.delete(key); + dbSourceField.set(store, originalDbSource); + + java.lang.reflect.Method initMethod = + CheckPointV2Store.class.getDeclaredMethod("init"); + initMethod.setAccessible(true); + initMethod.invoke(store); + } finally { + store.close(); + } + } + + @Test + public void testCloseWithRealResources() { + CheckPointV2Store store = new CheckPointV2Store("test-close-real"); + // Exercises the real writeOptions.close() and dbSource.closeDB() code paths + store.close(); + } + + @Test + public void testCloseReleasesAllResources() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close"); + + // Replace dbSource with a mock so we can verify closeDB() + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } + + @Test + public void testCloseWhenDbSourceThrows() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close-dbsource-throws"); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + doThrow(new RuntimeException("simulated dbSource failure")).when(mockDbSource).closeDB(); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + } finally { + originalDbSource.closeDB(); + } + } + + @Test + public void testCloseDbSourceWhenWriteOptionsThrows() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close-exception"); + + // Replace child writeOptions with a spy that throws on close + Field childWriteOptionsField = CheckPointV2Store.class.getDeclaredField("writeOptions"); + childWriteOptionsField.setAccessible(true); + WriteOptionsWrapper childWriteOptions = + (WriteOptionsWrapper) childWriteOptionsField.get(store); + WriteOptionsWrapper spyChildWriteOptions = spy(childWriteOptions); + doThrow(new RuntimeException("simulated writeOptions failure")) + .when(spyChildWriteOptions).close(); + childWriteOptionsField.set(store, spyChildWriteOptions); + + // Replace parent writeOptions with a spy that throws on close + Field parentWriteOptionsField = TronDatabase.class.getDeclaredField("writeOptions"); + parentWriteOptionsField.setAccessible(true); + WriteOptionsWrapper parentWriteOptions = + (WriteOptionsWrapper) parentWriteOptionsField.get(store); + WriteOptionsWrapper spyParentWriteOptions = spy(parentWriteOptions); + doThrow(new RuntimeException("simulated parent writeOptions failure")) + .when(spyParentWriteOptions).close(); + parentWriteOptionsField.set(store, spyParentWriteOptions); + + // Replace dbSource with a mock + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + + // dbSource.closeDB() must be called even though both writeOptions threw + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java b/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java index 52adf8e49fa..6bf479c287f 100644 --- a/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java +++ b/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java @@ -1,7 +1,13 @@ package org.tron.core.db; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; +import java.lang.reflect.Field; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -12,7 +18,9 @@ import org.junit.rules.TemporaryFolder; import org.rocksdb.RocksDB; import org.tron.common.TestConstants; +import org.tron.common.storage.WriteOptionsWrapper; import org.tron.core.config.args.Args; +import org.tron.core.db.common.DbSourceInter; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; @@ -99,4 +107,48 @@ public void TestGetFromRoot() throws Assert.assertEquals(db.getFromRoot("test".getBytes()), "test"); } + + @Test + public void testDoCloseDbSourceCalledWhenWriteOptionsThrows() throws Exception { + TronDatabase db = new TronDatabase("test-do-close") { + + @Override + public void put(byte[] key, String item) { + } + + @Override + public void delete(byte[] key) { + } + + @Override + public String get(byte[] key) { + return null; + } + + @Override + public boolean has(byte[] key) { + return false; + } + }; + + Field writeOptionsField = TronDatabase.class.getDeclaredField("writeOptions"); + writeOptionsField.setAccessible(true); + WriteOptionsWrapper spyWriteOptions = spy((WriteOptionsWrapper) writeOptionsField.get(db)); + doThrow(new RuntimeException("simulated writeOptions failure")).when(spyWriteOptions).close(); + writeOptionsField.set(db, spyWriteOptions); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(db); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(db, mockDbSource); + + try { + db.doClose(); + verify(spyWriteOptions).close(); + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } } diff --git a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java index d1fb95f2f69..6f0f601827c 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java @@ -90,6 +90,7 @@ public static void init() { @Before public void before() throws IOException { + EventPluginLoader.getInstance().setFilterQuery(null); Args.getInstance().setNodeListenPort(10000 + port.incrementAndGet()); dbManager = context.getBean(Manager.class); From 980c707f696c1adcb6aa01fc2ba7dfb9f04036e6 Mon Sep 17 00:00:00 2001 From: GrapeS <156868965+Little-Peony@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:07:11 +0800 Subject: [PATCH 28/43] fix(framework): fix SolidityNode shutdown and improve test quality (#6655) --- .../java/org/tron/program/SolidityNode.java | 43 +- .../common/ClassLevelAppContextFixture.java | 62 ++ .../tron/common/backup/BackupServerTest.java | 2 +- .../logsfilter/EventParserJsonTest.java | 1 - .../common/logsfilter/EventParserTest.java | 1 - .../logsfilter/NativeMessageQueueTest.java | 20 +- .../capsule/BlockFilterCapsuleTest.java | 1 - .../runtime/vm/BandWidthRuntimeTest.java | 10 +- .../tron/common/runtime/vm/FreezeTest.java | 8 - .../runtime/vm/VoteWitnessCost3Test.java | 2 - .../org/tron/common/utils/PropUtilTest.java | 19 +- .../java/org/tron/core/ShieldWalletTest.java | 28 +- .../test/java/org/tron/core/WalletTest.java | 25 +- .../actuator/FreezeBalanceActuatorTest.java | 30 - .../actuator/FreezeBalanceV2ActuatorTest.java | 27 - .../MarketCancelOrderActuatorTest.java | 36 +- .../UnDelegateResourceActuatorTest.java | 60 +- .../actuator/UnfreezeBalanceActuatorTest.java | 9 +- .../actuator/VoteWitnessActuatorTest.java | 16 +- .../core/actuator/utils/ProposalUtilTest.java | 724 +++++++----------- .../core/capsule/utils/MerkleTreeTest.java | 9 +- .../tron/core/capsule/utils/RLPListTest.java | 8 +- .../tron/core/db/AccountAssetStoreTest.java | 6 +- .../org/tron/core/db/AccountStoreTest.java | 9 +- .../java/org/tron/core/db/BlockStoreTest.java | 47 +- .../java/org/tron/core/db/DBIteratorTest.java | 59 +- .../java/org/tron/core/db/ManagerTest.java | 18 +- .../org/tron/core/db2/SnapshotImplTest.java | 19 +- .../tron/core/event/BlockEventCacheTest.java | 18 +- .../tron/core/event/BlockEventGetTest.java | 89 ++- .../core/jsonrpc/ConcurrentHashMapTest.java | 84 +- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 354 ++++----- .../tron/core/jsonrpc/LogBlockQueryTest.java | 19 +- .../core/jsonrpc/SectionBloomStoreTest.java | 18 +- .../java/org/tron/core/net/MessageTest.java | 15 +- .../test/java/org/tron/core/net/NodeTest.java | 24 +- .../FetchInvDataMsgHandlerTest.java | 37 +- .../InventoryMsgHandlerTest.java | 1 + .../messagehandler/MessageHandlerTest.java | 8 +- .../core/net/peer/PeerConnectionTest.java | 14 +- .../tron/core/net/peer/PeerManagerTest.java | 13 +- .../net/peer/PeerStatusCheckMockTest.java | 26 +- .../core/net/services/RelayServiceTest.java | 1 - .../net/services/TronStatsManagerTest.java | 16 +- .../java/org/tron/core/pbft/PbftTest.java | 4 +- .../tron/core/services/ComputeRewardTest.java | 7 + .../core/services/DelegationServiceTest.java | 1 - .../core/services/RpcApiServicesTest.java | 33 +- .../org/tron/core/services/WalletApiTest.java | 24 +- .../LiteFnQueryGrpcInterceptorTest.java | 25 +- .../filter/RpcApiAccessInterceptorTest.java | 24 +- .../ratelimiter/adaptor/AdaptorTest.java | 16 +- .../ratelimiter/adaptor/AdaptorThread.java | 2 +- .../core/utils/TransactionRegisterTest.java | 44 +- .../core/witness/ProposalControllerTest.java | 41 +- .../org/tron/keystore/CredentialsTest.java | 15 +- .../org/tron/program/SolidityNodeTest.java | 101 +++ .../java/org/tron/plugins/DbLiteTest.java | 4 +- .../java/org/tron/plugins/DbMoveTest.java | 1 - .../test/java/org/tron/plugins/DbTest.java | 4 +- .../plugins/leveldb/ArchiveManifestTest.java | 46 -- .../tron/plugins/leveldb/DbArchiveTest.java | 46 -- .../plugins/rocksdb/DbLiteRocksDbV2Test.java | 2 +- 63 files changed, 1067 insertions(+), 1409 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java index 3367141e2a5..6ffa3b3ce92 100644 --- a/framework/src/main/java/org/tron/program/SolidityNode.java +++ b/framework/src/main/java/org/tron/program/SolidityNode.java @@ -2,7 +2,10 @@ 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; @@ -11,6 +14,7 @@ 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; @@ -39,6 +43,9 @@ public class SolidityNode { private volatile boolean flag = true; + private ExecutorService getBlockEs; + private ExecutorService processBlockEs; + public SolidityNode(Manager dbManager) { this.dbManager = dbManager; this.chainBaseManager = dbManager.getChainBaseManager(); @@ -72,13 +79,26 @@ public static void start() { appT.startup(); SolidityNode node = new SolidityNode(appT.getDbManager()); node.run(); - appT.blockUntilShutdown(); + 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 { - new Thread(this::getBlock).start(); - new Thread(this::processBlock).start(); + 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) { @@ -88,6 +108,15 @@ private void run() { } } + 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) { @@ -137,7 +166,7 @@ private void loopProcessBlock(Block block) { } private Block getBlockByNum(long blockNum) { - while (true) { + while (flag) { try { long time = System.currentTimeMillis(); Block block = databaseGrpcClient.getBlock(blockNum); @@ -155,10 +184,11 @@ private Block getBlockByNum(long blockNum) { sleep(exceptionSleepTime); } } + return null; } private long getLastSolidityBlockNum() { - while (true) { + while (flag) { try { long time = System.currentTimeMillis(); long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum(); @@ -171,6 +201,7 @@ private long getLastSolidityBlockNum() { sleep(exceptionSleepTime); } } + return 0; } public void sleep(long time) { @@ -193,4 +224,4 @@ private void resolveCompatibilityIssueIfUsingFullNodeDatabase() { chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum); } } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java b/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java new file mode 100644 index 00000000000..1d26f895b64 --- /dev/null +++ b/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java @@ -0,0 +1,62 @@ +package org.tron.common; + +import io.grpc.ManagedChannel; +import java.util.concurrent.TimeUnit; +import org.tron.common.application.ApplicationFactory; +import org.tron.common.application.TronApplicationContext; +import org.tron.core.config.DefaultConfig; + +/** + * Shared class-level fixture for tests that manually manage a TronApplicationContext. + */ +public class ClassLevelAppContextFixture { + + private TronApplicationContext context; + + public TronApplicationContext createContext() { + context = new TronApplicationContext(DefaultConfig.class); + return context; + } + + public TronApplicationContext createAndStart() { + createContext(); + startApp(); + return context; + } + + public void startApp() { + ApplicationFactory.create(context).startup(); + } + + public TronApplicationContext getContext() { + return context; + } + + public void close() { + if (context != null) { + context.close(); + context = null; + } + } + + public static void shutdownChannel(ManagedChannel channel) { + if (channel == null) { + return; + } + try { + channel.shutdown(); + if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { + channel.shutdownNow(); + } + } catch (InterruptedException e) { + channel.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public static void shutdownChannels(ManagedChannel... channels) { + for (ManagedChannel channel : channels) { + shutdownChannel(channel); + } + } +} diff --git a/framework/src/test/java/org/tron/common/backup/BackupServerTest.java b/framework/src/test/java/org/tron/common/backup/BackupServerTest.java index ae5f74d8b71..50778970d87 100644 --- a/framework/src/test/java/org/tron/common/backup/BackupServerTest.java +++ b/framework/src/test/java/org/tron/common/backup/BackupServerTest.java @@ -46,7 +46,7 @@ public void tearDown() { @Test(timeout = 60_000) public void test() throws InterruptedException { backupServer.initServer(); - // wait for the server to start + // wait for the server to start so channel is assigned before close() is called Thread.sleep(1000); } } diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java index 34a8e82c424..9b0f17244a8 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java @@ -65,7 +65,6 @@ public synchronized void testEventParser() { for (int i = 0; i < entryArr.size(); i++) { JSONObject e = entryArr.getJSONObject(i); - System.out.println(e.getString("name")); if (e.getString("name") != null) { if (e.getString("name").equalsIgnoreCase("eventBytesL")) { entry = e; diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java index eff644b9cd9..0d2a8ed1496 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -68,7 +68,6 @@ public synchronized void testEventParser() { ABI.Entry entry = null; for (ABI.Entry e : abi.getEntrysList()) { - System.out.println(e.getName()); if (e.getName().equalsIgnoreCase("eventBytesL")) { entry = e; break; diff --git a/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java b/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java index d356e43d66c..5219654977b 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java @@ -1,7 +1,10 @@ package org.tron.common.logsfilter; +import java.util.concurrent.ExecutorService; +import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.logsfilter.nativequeue.NativeMessageQueue; import org.zeromq.SocketType; import org.zeromq.ZContext; @@ -13,6 +16,15 @@ public class NativeMessageQueueTest { public String dataToSend = "################"; public String topic = "testTopic"; + private ExecutorService subscriberExecutor; + private final String zmqSubscriber = "zmq-subscriber"; + + @After + public void tearDown() { + ExecutorServiceManager.shutdownAndAwaitTermination(subscriberExecutor, zmqSubscriber); + subscriberExecutor = null; + } + @Test public void invalidBindPort() { boolean bRet = NativeMessageQueue.getInstance().start(-1111, 0); @@ -39,7 +51,7 @@ public void publishTrigger() { try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } NativeMessageQueue.getInstance().publishTrigger(dataToSend, topic); @@ -47,14 +59,15 @@ public void publishTrigger() { try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } NativeMessageQueue.getInstance().stop(); } public void startSubscribeThread() { - Thread thread = new Thread(() -> { + subscriberExecutor = ExecutorServiceManager.newSingleThreadExecutor(zmqSubscriber); + subscriberExecutor.execute(() -> { try (ZContext context = new ZContext()) { ZMQ.Socket subscriber = context.createSocket(SocketType.SUB); @@ -70,6 +83,5 @@ public void startSubscribeThread() { // ZMQ.Socket will be automatically closed when ZContext is closed } }); - thread.start(); } } 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 5381c6ab2de..b5f7e676eea 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 @@ -22,7 +22,6 @@ public void setUp() { public void testSetAndGetBlockHash() { blockFilterCapsule .setBlockHash("e58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f"); - System.out.println(blockFilterCapsule); Assert.assertEquals("e58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f", blockFilterCapsule.getBlockHash()); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java index 40a4003f625..8e38c08c4d8 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java @@ -24,6 +24,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.Commons; @@ -153,8 +154,13 @@ public void testSuccess() { @Test public void testSuccessNoBandd() { + boolean originalDebug = CommonParameter.getInstance().isDebug(); try { byte[] contractAddress = createContract(); + // Enable debug mode to bypass CPU time limit check in Program.checkCPUTimeLimit(). + // Without this, the heavy contract execution (setCoin) may exceed the time threshold + // on slow machines and cause the test to fail non-deterministically. + CommonParameter.getInstance().setDebug(true); TriggerSmartContract triggerContract = TvmTestUtils.createTriggerContract(contractAddress, "setCoin(uint256)", "50", false, 0, Commons.decodeFromBase58Check(TriggerOwnerTwoAddress)); @@ -185,6 +191,8 @@ public void testSuccessNoBandd() { balance); } catch (TronException e) { Assert.assertNotNull(e); + } finally { + CommonParameter.getInstance().setDebug(originalDebug); } } @@ -254,4 +262,4 @@ public void testMaxContractResultSize() { } Assert.assertEquals(2, maxSize); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java b/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java index 0d6305f8782..071c07bdc9e 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java @@ -289,17 +289,9 @@ public void testWithCallerEnergyChangedInTx() throws Exception { TVMTestResult result = freezeForOther(userA, contractAddr, userA, frozenBalance, 1); - System.out.println(result.getReceipt().getEnergyUsageTotal()); - System.out.println(accountStore.get(userA)); - System.out.println(accountStore.get(owner)); - clearDelegatedExpireTime(contractAddr, userA); result = unfreezeForOther(userA, contractAddr, userA, 1); - - System.out.println(result.getReceipt().getEnergyUsageTotal()); - System.out.println(accountStore.get(userA)); - System.out.println(accountStore.get(owner)); } @Test diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java index 66de45a0658..75b11f4ab9d 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java @@ -124,8 +124,6 @@ public void testZeroLengthOneArray() { // witness array zero, amount array non-zero Program program = mockProgram(0, 0, 64, 1, 0); long cost = EnergyCost.getVoteWitnessCost3(program); - // witnessArraySize = 0 * 32 + 32 = 32, witnessMemNeeded = 0 + 32 = 32 - // amountArraySize = 1 * 32 + 32 = 64, amountMemNeeded = 64 + 64 = 128 // memWords = 128 / 32 = 4 // memEnergy = 3 * 4 + 4 * 4 / 512 = 12 assertEquals(30012, cost); diff --git a/framework/src/test/java/org/tron/common/utils/PropUtilTest.java b/framework/src/test/java/org/tron/common/utils/PropUtilTest.java index 2df5bd8effd..bc4d15a4df7 100644 --- a/framework/src/test/java/org/tron/common/utils/PropUtilTest.java +++ b/framework/src/test/java/org/tron/common/utils/PropUtilTest.java @@ -1,21 +1,16 @@ package org.tron.common.utils; import java.io.File; -import java.io.IOException; import org.junit.Assert; import org.junit.Test; public class PropUtilTest { @Test - public void testWriteProperty() { + public void testWriteProperty() throws Exception { String filename = "test_prop.properties"; File file = new File(filename); - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } + file.createNewFile(); PropUtil.writeProperty(filename, "key", "value"); Assert.assertTrue("value".equals(PropUtil.readProperty(filename, "key"))); PropUtil.writeProperty(filename, "key", "value2"); @@ -24,17 +19,13 @@ public void testWriteProperty() { } @Test - public void testReadProperty() { + public void testReadProperty() throws Exception { String filename = "test_prop.properties"; File file = new File(filename); - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } + file.createNewFile(); PropUtil.writeProperty(filename, "key", "value"); Assert.assertTrue("value".equals(PropUtil.readProperty(filename, "key"))); file.delete(); Assert.assertTrue("".equals(PropUtil.readProperty(filename, "key"))); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/ShieldWalletTest.java b/framework/src/test/java/org/tron/core/ShieldWalletTest.java index 903309510a8..b9fa48dca38 100644 --- a/framework/src/test/java/org/tron/core/ShieldWalletTest.java +++ b/framework/src/test/java/org/tron/core/ShieldWalletTest.java @@ -369,14 +369,12 @@ public void testCreateShieldedContractParameters2() throws ContractExeException Assert.fail(); } - try { - wallet1.createShieldedContractParameters(builder.build()); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("PaymentAddress in ReceiveNote should not be empty", - e.getMessage()); - } + PrivateShieldedTRC20Parameters.Builder finalBuilder = builder; + Exception e1 = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(finalBuilder.build())); + Assert.assertTrue(e1 instanceof ContractValidateException); + Assert.assertEquals("PaymentAddress in ReceiveNote should not be empty", + e1.getMessage()); String parameter2 = new String(ByteArray.fromHexString( "7b0a202020202261736b223a2263323531336539653330383439343933326264383265306365353336" @@ -402,14 +400,12 @@ public void testCreateShieldedContractParameters2() throws ContractExeException Assert.fail(); } - try { - wallet1.createShieldedContractParameters(builder.build()); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("PaymentAddress in SpendNote should not be empty", - e.getMessage()); - } + PrivateShieldedTRC20Parameters.Builder finalBuilder1 = builder; + Exception e2 = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(finalBuilder1.build())); + Assert.assertTrue(e2 instanceof ContractValidateException); + Assert.assertEquals("PaymentAddress in SpendNote should not be empty", + e2.getMessage()); } @Test diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index 44e25a16387..0df8d6cdc2c 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -860,15 +860,12 @@ public void testGetDelegatedResourceV2() { @Test public void testGetPaginatedNowWitnessList_Error() { - try { - // To avoid throw MaintenanceClearingException - dbManager.getChainBaseManager().getDynamicPropertiesStore().saveStateFlag(1); - wallet.getPaginatedNowWitnessList(0, 10); - Assert.fail("Should throw error when in maintenance period"); - } catch (Exception e) { - Assert.assertTrue("Should throw MaintenanceClearingException", - e instanceof MaintenanceUnavailableException); - } + // To avoid throw MaintenanceClearingException + dbManager.getChainBaseManager().getDynamicPropertiesStore().saveStateFlag(1); + Exception maintenanceEx = Assert.assertThrows(Exception.class, + () -> wallet.getPaginatedNowWitnessList(0, 10)); + Assert.assertTrue("Should throw MaintenanceClearingException", + maintenanceEx instanceof MaintenanceUnavailableException); try { Args.getInstance().setSolidityNode(true); @@ -1376,13 +1373,9 @@ public void testEstimateEnergyOutOfTime() { Args.getInstance().setEstimateEnergy(true); - try { - wallet.estimateEnergy( - contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder); - Assert.fail("EstimateEnergy should throw exception!"); - } catch (Program.OutOfTimeException ignored) { - Assert.assertTrue(true); - } + Assert.assertThrows(Program.OutOfTimeException.class, + () -> wallet.estimateEnergy( + contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder)); } @Test diff --git a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java index ddcb9976200..c830cd091e6 100644 --- a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java @@ -22,7 +22,6 @@ import org.tron.core.capsule.DelegatedResourceAccountIndexCapsule; import org.tron.core.capsule.DelegatedResourceCapsule; import org.tron.core.capsule.TransactionResultCapsule; -import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.config.args.Args; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; @@ -621,35 +620,6 @@ public void frozenNumTest() { } } - //@Test - public void moreThanFrozenNumber() { - long frozenBalance = 1_000_000_000L; - long duration = 3; - FreezeBalanceActuator actuator = new FreezeBalanceActuator(); - actuator.setChainBaseManager(dbManager.getChainBaseManager()) - .setAny(getContractForBandwidth(OWNER_ADDRESS, frozenBalance, duration)); - - TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - actuator.execute(ret); - } catch (ContractValidateException | ContractExeException e) { - Assert.fail(); - } - try { - actuator.validate(); - actuator.execute(ret); - fail("cannot run here."); - } catch (ContractValidateException e) { - long maxFrozenNumber = ChainConstant.MAX_FROZEN_NUMBER; - Assert.assertEquals("max frozen number is: " + maxFrozenNumber, e.getMessage()); - - } catch (ContractExeException e) { - Assert.fail(); - } - } - - @Test public void commonErrorCheck() { FreezeBalanceActuator actuator = new FreezeBalanceActuator(); diff --git a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java index 24585326110..92e7cfa78ca 100644 --- a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java @@ -268,33 +268,6 @@ public void lessThan1TrxTest() { } } - //@Test - public void moreThanFrozenNumber() { - long frozenBalance = 1_000_000_000L; - FreezeBalanceActuator actuator = new FreezeBalanceActuator(); - actuator.setChainBaseManager(dbManager.getChainBaseManager()) - .setAny(getContractV2ForBandwidth(OWNER_ADDRESS, frozenBalance)); - - TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - actuator.execute(ret); - } catch (ContractValidateException | ContractExeException e) { - Assert.fail(); - } - try { - actuator.validate(); - actuator.execute(ret); - fail("cannot run here."); - } catch (ContractValidateException e) { - long maxFrozenNumber = ChainConstant.MAX_FROZEN_NUMBER; - Assert.assertEquals("max frozen number is: " + maxFrozenNumber, e.getMessage()); - } catch (ContractExeException e) { - Assert.fail(); - } - } - - @Test public void commonErrorCheck() { FreezeBalanceV2Actuator actuator = new FreezeBalanceV2Actuator(); diff --git a/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java index d2ec8c30994..4966ef67987 100644 --- a/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java @@ -188,12 +188,9 @@ public void invalidOwnerAddress() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_INVALID, orderId)); - try { - actuator.validate(); - Assert.fail("Invalid address"); - } catch (ContractValidateException e) { - Assert.assertEquals("Invalid address", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Invalid address", e.getMessage()); } /** @@ -208,12 +205,9 @@ public void notExistAccount() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_NOT_EXIST, orderId)); - try { - actuator.validate(); - Assert.fail("Account does not exist!"); - } catch (ContractValidateException e) { - Assert.assertEquals("Account does not exist!", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Account does not exist!", e.getMessage()); } /** @@ -227,12 +221,9 @@ public void notExistOrder() { MarketCancelOrderActuator actuator = new MarketCancelOrderActuator(); actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_FIRST, orderId)); - try { - actuator.validate(); - Assert.fail("orderId not exists"); - } catch (ContractValidateException e) { - Assert.assertEquals("orderId not exists", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("orderId not exists", e.getMessage()); } /** @@ -261,12 +252,9 @@ public void orderNotActive() throws Exception { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_FIRST, orderId)); - try { - actuator.validate(); - Assert.fail("Order is not active!"); - } catch (ContractValidateException e) { - Assert.assertEquals("Order is not active!", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Order is not active!", e.getMessage()); } diff --git a/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java index f3211c8b8eb..344a4e95f30 100644 --- a/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java @@ -464,29 +464,25 @@ public void testLockedUnDelegateBalanceForBandwidthInsufficient() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny( getDelegatedContractForBandwidth(OWNER_ADDRESS, delegateBalance)); - try { - ownerCapsule = dbManager.getAccountStore().get(owner); - Assert.assertEquals(delegateBalance, - receiverCapsule.getAcquiredDelegatedFrozenV2BalanceForBandwidth()); - Assert.assertEquals(delegateBalance, ownerCapsule.getDelegatedFrozenV2BalanceForBandwidth()); - Assert.assertEquals(0, ownerCapsule.getFrozenV2BalanceForBandwidth()); - Assert.assertEquals(delegateBalance, ownerCapsule.getTronPower()); - Assert.assertEquals(1_000_000_000, ownerCapsule.getNetUsage()); - Assert.assertEquals(1_000_000_000, receiverCapsule.getNetUsage()); - DelegatedResourceCapsule delegatedResourceCapsule = dbManager.getDelegatedResourceStore() - .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, false)); - DelegatedResourceCapsule lockedResourceCapsule = dbManager.getDelegatedResourceStore() - .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, true)); - Assert.assertNull(delegatedResourceCapsule); - Assert.assertNotNull(lockedResourceCapsule); - - actuator.validate(); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("insufficient delegatedFrozenBalance(BANDWIDTH), " - + "request=1000000000, unlock_balance=0", e.getMessage()); - } + ownerCapsule = dbManager.getAccountStore().get(owner); + Assert.assertEquals(delegateBalance, + receiverCapsule.getAcquiredDelegatedFrozenV2BalanceForBandwidth()); + Assert.assertEquals(delegateBalance, ownerCapsule.getDelegatedFrozenV2BalanceForBandwidth()); + Assert.assertEquals(0, ownerCapsule.getFrozenV2BalanceForBandwidth()); + Assert.assertEquals(delegateBalance, ownerCapsule.getTronPower()); + Assert.assertEquals(1_000_000_000, ownerCapsule.getNetUsage()); + Assert.assertEquals(1_000_000_000, receiverCapsule.getNetUsage()); + DelegatedResourceCapsule delegatedResourceCapsule = dbManager.getDelegatedResourceStore() + .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, false)); + DelegatedResourceCapsule lockedResourceCapsule = dbManager.getDelegatedResourceStore() + .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, true)); + Assert.assertNull(delegatedResourceCapsule); + Assert.assertNotNull(lockedResourceCapsule); + + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("insufficient delegatedFrozenBalance(BANDWIDTH), " + + "request=1000000000, unlock_balance=0", e.getMessage()); } @Test @@ -976,22 +972,16 @@ public void noDelegateBalance() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getDelegatedContractForBandwidth(OWNER_ADDRESS, delegateBalance)); - try { - actuator.validate(); - Assert.fail("cannot run here."); - } catch (ContractValidateException e) { - Assert.assertEquals("delegated Resource does not exist", e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("delegated Resource does not exist", e1.getMessage()); actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getDelegatedContractForCpu(delegateBalance)); - try { - actuator.validate(); - Assert.fail("cannot run here."); - } catch (ContractValidateException e) { - Assert.assertEquals("delegated Resource does not exist", e.getMessage()); - } + ContractValidateException e2 = + Assert.assertThrows(ContractValidateException.class, () -> actuator.validate()); + Assert.assertEquals("delegated Resource does not exist", e2.getMessage()); } @Test diff --git a/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java index f5c65bf381f..7f74ee3fcc5 100644 --- a/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java @@ -1166,12 +1166,9 @@ public void testUnfreezeBalanceForTronPowerWithOldTronPowerAfterNewResourceModel actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContractForTronPower(OWNER_ADDRESS)); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("It's not time to unfreeze(TronPower).", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("It's not time to unfreeze(TronPower).", e.getMessage()); } } diff --git a/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java index 6ec72043722..9823c3aba51 100644 --- a/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java @@ -569,12 +569,8 @@ public void voteWitnessWithoutEnoughOldTronPowerAfterNewResourceModel() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContract(OWNER_ADDRESS, WITNESS_ADDRESS, 100L)); TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertTrue(e instanceof ContractValidateException); - } + Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(0L); } @@ -658,12 +654,8 @@ public void voteWitnessWithoutEnoughOldAndNewTronPowerAfterNewResourceModel() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContract(OWNER_ADDRESS, WITNESS_ADDRESS, 4000000L)); TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertTrue(e instanceof ContractValidateException); - } + Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(0L); } diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index f8d8e6bdd9d..c26da5a7bf7 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -62,15 +62,12 @@ public void validProposalTypeCheck() throws ContractValidateException { Assert.assertNull(ProposalType.getEnumOrNull(-2)); Assert.assertEquals(ProposalType.ALLOW_TVM_SOLIDITY_059, ProposalType.getEnumOrNull(32)); - long code = -1; - try { - ProposalType.getEnum(code); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Does not support code : " + code, e.getMessage()); - } + long finalCode = -1; + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> ProposalType.getEnum(finalCode)); + Assert.assertEquals("Does not support code : " + finalCode, e.getMessage()); - code = 32; + long code = 32; Assert.assertEquals(ProposalType.ALLOW_TVM_SOLIDITY_059, ProposalType.getEnum(code)); } @@ -79,217 +76,145 @@ public void validProposalTypeCheck() throws ContractValidateException { public void validateCheck() { long invalidValue = -1; - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ACCOUNT_UPGRADE_COST.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ACCOUNT_UPGRADE_COST.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_ACCOUNT_FEE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_ACCOUNT_FEE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ASSET_ISSUE_FEE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ASSET_ISSUE_FEE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 3 * 27 * 1000 - 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 24 * 3600 * 1000 + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_CREATION_OF_CONTRACTS.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_CREATION_OF_CONTRACTS] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ACCOUNT_UPGRADE_COST.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e1.getMessage()); + + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ACCOUNT_UPGRADE_COST.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e2.getMessage()); + + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_ACCOUNT_FEE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e3.getMessage()); + + ContractValidateException e4 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_ACCOUNT_FEE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e4.getMessage()); + + ContractValidateException e5 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ASSET_ISSUE_FEE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e5.getMessage()); + + ContractValidateException e6 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ASSET_ISSUE_FEE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e6.getMessage()); + + ContractValidateException e7 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e7.getMessage()); + + ContractValidateException e8 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e8.getMessage()); + + ContractValidateException e9 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e9.getMessage()); + + ContractValidateException e10 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e10.getMessage()); + + ContractValidateException e11 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e11.getMessage()); + + ContractValidateException e12 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e12.getMessage()); + + ContractValidateException e13 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e13.getMessage()); + + ContractValidateException e14 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e14.getMessage()); + + ContractValidateException e15 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 3 * 27 * 1000 - 1)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", + e15.getMessage()); + + ContractValidateException e16 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 24 * 3600 * 1000 + 1)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", + e16.getMessage()); + + ContractValidateException e17 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_CREATION_OF_CONTRACTS.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_CREATION_OF_CONTRACTS] is only allowed to be 1", + e17.getMessage()); dynamicPropertiesStore = dbManager.getDynamicPropertiesStore(); dynamicPropertiesStore.saveRemoveThePowerOfTheGr(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[REMOVE_THE_POWER_OF_THE_GR] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e18 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 2)); + Assert.assertEquals( + "This value[REMOVE_THE_POWER_OF_THE_GR] is only allowed to be 1", + e18.getMessage()); dynamicPropertiesStore.saveRemoveThePowerOfTheGr(-1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This proposal has been executed before and is only allowed to be executed once", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 9); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [10,100]", e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 101); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [10,100]", e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_DELEGATE_RESOURCE.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_DELEGATE_RESOURCE] is only allowed to be 1", e.getMessage()); - } + ContractValidateException e19 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 1)); + Assert.assertEquals( + "This proposal has been executed before and is only allowed to be executed once", + e19.getMessage()); + + ContractValidateException e20 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 9)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [10,100]", e20.getMessage()); + + ContractValidateException e21 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 101)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [10,100]", e21.getMessage()); + + ContractValidateException e22 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_DELEGATE_RESOURCE.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_DELEGATE_RESOURCE] is only allowed to be 1", e22.getMessage()); dynamicPropertiesStore.saveAllowSameTokenName(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_TRANSFER_TRC10] is only allowed to be 1", e.getMessage()); - } + ContractValidateException e23 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_TRANSFER_TRC10] is only allowed to be 1", e23.getMessage()); dynamicPropertiesStore.saveAllowSameTokenName(0); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("[ALLOW_SAME_TOKEN_NAME] proposal must be approved " - + "before [ALLOW_TVM_TRANSFER_TRC10] can be proposed", e.getMessage()); - } + ContractValidateException e24 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 1)); + Assert.assertEquals("[ALLOW_SAME_TOKEN_NAME] proposal must be approved " + + "before [ALLOW_TVM_TRANSFER_TRC10] can be proposed", e24.getMessage()); forkUtils.init(dbManager.getChainBaseManager()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() @@ -308,15 +233,12 @@ public void validateCheck() { List w = new ArrayList<>(); w.add(address); forkUtils.getManager().getWitnessScheduleStore().saveActiveWitnesses(w); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_SHIELDED_TRC20_TRANSACTION - .getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("This value[ALLOW_SHIELDED_TRC20_TRANSACTION] is only allowed" - + " to be 1 or 0", e.getMessage()); - } + ContractValidateException e25 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_SHIELDED_TRC20_TRANSACTION + .getCode(), 2)); + Assert.assertEquals("This value[ALLOW_SHIELDED_TRC20_TRANSACTION] is only allowed" + + " to be 1 or 0", e25.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_3.getHardForkTime() - 1) / maintenanceTimeInterval + 1) @@ -325,33 +247,24 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_3.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.FREE_NET_LIMIT - .getCode(), -1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Bad chain parameter value, valid range is [0,100_000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.TOTAL_NET_LIMIT.getCode(), -1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Bad chain parameter value, valid range is [0, 1_000_000_000_000L]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_OLD_REWARD_OPT]", - e.getMessage()); - } + ContractValidateException e26 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.FREE_NET_LIMIT + .getCode(), -1)); + Assert.assertEquals("Bad chain parameter value, valid range is [0,100_000]", + e26.getMessage()); + + ContractValidateException e27 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.TOTAL_NET_LIMIT.getCode(), -1)); + Assert.assertEquals("Bad chain parameter value, valid range is [0, 1_000_000_000_000L]", + e27.getMessage()); + + ContractValidateException e28 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_OLD_REWARD_OPT]", + e28.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_7_4.getHardForkTime() - 1) / maintenanceTimeInterval + 1) * maintenanceTimeInterval; @@ -359,47 +272,35 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_4.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_OLD_REWARD_OPT] is only allowed to be 1", - e.getMessage()); - } - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_NEW_REWARD] or [ALLOW_TVM_VOTE] proposal must be approved " - + "before [ALLOW_OLD_REWARD_OPT] can be proposed", - e.getMessage()); - } + ContractValidateException e29 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_OLD_REWARD_OPT] is only allowed to be 1", + e29.getMessage()); + ContractValidateException e30 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_NEW_REWARD] or [ALLOW_TVM_VOTE] proposal must be approved " + + "before [ALLOW_OLD_REWARD_OPT] can be proposed", + e30.getMessage()); dynamicPropertiesStore.put("NEW_REWARD_ALGORITHM_EFFECTIVE_CYCLE".getBytes(), new BytesCapsule(ByteArray.fromLong(4000))); dynamicPropertiesStore.saveAllowOldRewardOpt(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_OLD_REWARD_OPT] has been valid, no need to propose again", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_STRICT_MATH]", - e.getMessage()); - } + ContractValidateException e31 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_OLD_REWARD_OPT] has been valid, no need to propose again", + e31.getMessage()); + + ContractValidateException e32 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 2)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_STRICT_MATH]", + e32.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_7_7.getHardForkTime() - 1) / maintenanceTimeInterval + 1) * maintenanceTimeInterval; @@ -407,15 +308,12 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_7.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_STRICT_MATH] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e33 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_STRICT_MATH] is only allowed to be 1", + e33.getMessage()); try { ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.ALLOW_STRICT_MATH.getCode(), 1); @@ -426,15 +324,12 @@ public void validateCheck() { ProposalType.ALLOW_STRICT_MATH.getCode(), 1).build(); ProposalCapsule proposalCapsule = new ProposalCapsule(proposal); ProposalService.process(dbManager, proposalCapsule); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_STRICT_MATH] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e34 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_STRICT_MATH] has been valid, no need to propose again", + e34.getMessage()); testEnergyAdjustmentProposal(); @@ -455,15 +350,12 @@ public void validateCheck() { private void testEnergyAdjustmentProposal() { // Should fail because cannot pass the fork controller check - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_ENERGY_ADJUSTMENT]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_ENERGY_ADJUSTMENT]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -480,15 +372,12 @@ private void testEnergyAdjustmentProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_5.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_ENERGY_ADJUSTMENT] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_ENERGY_ADJUSTMENT] is only allowed to be 1", + e2.getMessage()); // Should succeed try { @@ -504,27 +393,21 @@ private void testEnergyAdjustmentProposal() { proposalCapsule.setParameters(parameter); ProposalService.process(dbManager, proposalCapsule); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_ENERGY_ADJUSTMENT] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_ENERGY_ADJUSTMENT] has been valid, no need to propose again", + e3.getMessage()); } private void testConsensusLogicOptimizationProposal() { - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [CONSENSUS_LOGIC_OPTIMIZATION]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [CONSENSUS_LOGIC_OPTIMIZATION]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -541,26 +424,20 @@ private void testConsensusLogicOptimizationProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[CONSENSUS_LOGIC_OPTIMIZATION] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 2)); + Assert.assertEquals( + "This value[CONSENSUS_LOGIC_OPTIMIZATION] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveConsensusLogicOptimization(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[CONSENSUS_LOGIC_OPTIMIZATION] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1)); + Assert.assertEquals( + "[CONSENSUS_LOGIC_OPTIMIZATION] has been valid, no need to propose again", + e3.getMessage()); } @@ -568,15 +445,12 @@ private void testAllowTvmCancunProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_CANCUN]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_CANCUN]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -593,26 +467,20 @@ private void testAllowTvmCancunProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_CANCUN] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_CANCUN] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmCancun(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_CANCUN] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_CANCUN] has been valid, no need to propose again", + e3.getMessage()); } @@ -620,15 +488,12 @@ private void testAllowTvmBlobProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_BLOB]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_BLOB]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -645,26 +510,20 @@ private void testAllowTvmBlobProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_BLOB] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_BLOB] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmBlob(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_BLOB] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_BLOB] has been valid, no need to propose again", + e3.getMessage()); } @@ -672,15 +531,12 @@ private void testAllowTvmSelfdestructRestrictionProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_SELFDESTRUCT_RESTRICTION]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_SELFDESTRUCT_RESTRICTION]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -697,26 +553,20 @@ private void testAllowTvmSelfdestructRestrictionProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmSelfdestructRestriction(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] has been valid, no need to propose again", + e3.getMessage()); } private void testAllowMarketTransaction() { diff --git a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java index df84433726e..88e95f9653e 100644 --- a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java +++ b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java @@ -101,12 +101,9 @@ private static int getRank(int num) { */ public void test0HashNum() { List hashList = getHash(0); //Empty list. - try { - MerkleTree.getInstance().createTree(hashList); - Assert.assertFalse(true); - } catch (Exception e) { - Assert.assertTrue(e instanceof IndexOutOfBoundsException); - } + Exception e = Assert.assertThrows(Exception.class, + () -> MerkleTree.getInstance().createTree(hashList)); + Assert.assertTrue(e instanceof IndexOutOfBoundsException); } @Test diff --git a/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java b/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java index 500e7454dbe..9c2e8550634 100644 --- a/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java +++ b/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java @@ -69,12 +69,8 @@ public void testToBytes() Assert.assertArrayEquals(kBytes, lBytes); char c = 'a'; - try { - method.invoke(RLP.class, c); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(true); - } + Assert.assertThrows(Exception.class, + () -> method.invoke(RLP.class, c)); } @Test diff --git a/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java index 88734945687..41fdc2e3925 100644 --- a/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java @@ -84,11 +84,7 @@ private long createAsset(String tokenName) { AssetIssueCapsule assetIssueCapsule = new AssetIssueCapsule(assetIssueContract); chainBaseManager.getAssetIssueV2Store() .put(assetIssueCapsule.createDbV2Key(), assetIssueCapsule); - try { - ownerCapsule.addAssetV2(ByteArray.fromString(String.valueOf(id)), TOTAL_SUPPLY); - } catch (Exception e) { - e.printStackTrace(); - } + ownerCapsule.addAssetV2(ByteArray.fromString(String.valueOf(id)), TOTAL_SUPPLY); accountStore.put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); return id; } diff --git a/framework/src/test/java/org/tron/core/db/AccountStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountStoreTest.java index a908d5d3cea..2fae33870cb 100755 --- a/framework/src/test/java/org/tron/core/db/AccountStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountStoreTest.java @@ -77,12 +77,9 @@ public void setAccountTest() throws Exception { field.set(AccountStore.class, new HashMap<>()); Config config = mock(Config.class); Mockito.when(config.getObjectList("genesis.block.assets")).thenReturn(new ArrayList<>()); - try { - AccountStore.setAccount(config); - Assert.fail(); - } catch (Throwable e) { - Assert.assertTrue(e instanceof TronError); - } + Throwable e = Assert.assertThrows(Throwable.class, + () -> AccountStore.setAccount(config)); + Assert.assertTrue(e instanceof TronError); } @Test diff --git a/framework/src/test/java/org/tron/core/db/BlockStoreTest.java b/framework/src/test/java/org/tron/core/db/BlockStoreTest.java index 1868eae4cba..b85a6312278 100644 --- a/framework/src/test/java/org/tron/core/db/BlockStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/BlockStoreTest.java @@ -10,8 +10,6 @@ import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; -import org.tron.core.exception.BadItemException; -import org.tron.core.exception.ItemNotFoundException; @Slf4j @@ -35,56 +33,43 @@ public void testCreateBlockStore() { } @Test - public void testPut() { + public void testPut() throws Exception { long number = 1; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - BlockCapsule blockCapsule1 = blockStore.get(blockId); - Assert.assertNotNull(blockCapsule1); - Assert.assertEquals(number, blockCapsule1.getNum()); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertNotNull(blockCapsule1); + Assert.assertEquals(number, blockCapsule1.getNum()); } @Test - public void testGet() { + public void testGet() throws Exception { long number = 2; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - boolean has = blockStore.has(blockId); - Assert.assertTrue(has); - BlockCapsule blockCapsule1 = blockStore.get(blockId); - - Assert.assertEquals(number, blockCapsule1.getNum()); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + boolean has = blockStore.has(blockId); + Assert.assertTrue(has); + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertEquals(number, blockCapsule1.getNum()); } @Test - public void testDelete() { + public void testDelete() throws Exception { long number = 1; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - BlockCapsule blockCapsule1 = blockStore.get(blockId); - Assert.assertNotNull(blockCapsule1); - Assert.assertEquals(number, blockCapsule1.getNum()); + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertNotNull(blockCapsule1); + Assert.assertEquals(number, blockCapsule1.getNum()); - blockStore.delete(blockId); - BlockCapsule blockCapsule2 = blockStore.getUnchecked(blockId); - Assert.assertNull(blockCapsule2); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + blockStore.delete(blockId); + BlockCapsule blockCapsule2 = blockStore.getUnchecked(blockId); + Assert.assertNull(blockCapsule2); } } diff --git a/framework/src/test/java/org/tron/core/db/DBIteratorTest.java b/framework/src/test/java/org/tron/core/db/DBIteratorTest.java index 58923ce50b6..0966d904093 100644 --- a/framework/src/test/java/org/tron/core/db/DBIteratorTest.java +++ b/framework/src/test/java/org/tron/core/db/DBIteratorTest.java @@ -86,43 +86,36 @@ public void testRocksDb() throws RocksDBException, IOException { RocksDB db = RocksDB.open(options, file.toString())) { db.put("1".getBytes(StandardCharsets.UTF_8), "1".getBytes(StandardCharsets.UTF_8)); db.put("2".getBytes(StandardCharsets.UTF_8), "2".getBytes(StandardCharsets.UTF_8)); - RockStoreIterator iterator = new RockStoreIterator(db.newIterator(), new ReadOptions()); - iterator.seekToFirst(); - Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.getKey()); - Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.next().getValue()); - Assert.assertTrue(iterator.hasNext()); - - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.next().getKey()); - Assert.assertFalse(iterator.hasNext()); - - try { - iterator.seekToLast(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); + try (RockStoreIterator iterator = + new RockStoreIterator(db.newIterator(), new ReadOptions())) { + iterator.seekToFirst(); + Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.getKey()); + Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.next().getValue()); + Assert.assertTrue(iterator.hasNext()); + + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.next().getKey()); + Assert.assertFalse(iterator.hasNext()); + + Assert.assertThrows(IllegalStateException.class, iterator::seekToLast); } - iterator = new RockStoreIterator(db.newIterator(), new ReadOptions()); - iterator.seekToLast(); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getKey()); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); - iterator.seekToFirst(); - while (iterator.hasNext()) { + try ( + RockStoreIterator iterator = + new RockStoreIterator(db.newIterator(), new ReadOptions())) { + iterator.seekToLast(); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getKey()); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); + iterator.seekToFirst(); + while (iterator.hasNext()) { + iterator.next(); + } + Assert.assertFalse(iterator.hasNext()); + Assert.assertThrows(IllegalStateException.class, iterator::getKey); + Assert.assertThrows(IllegalStateException.class, iterator::getValue); + thrown.expect(NoSuchElementException.class); iterator.next(); } - Assert.assertFalse(iterator.hasNext()); - try { - iterator.getKey(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); - } - try { - iterator.getValue(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); - } - thrown.expect(NoSuchElementException.class); - iterator.next(); } } 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 b9808b89193..a07fb291f34 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -272,12 +272,8 @@ public void pushBlock() { } } - try { - chainManager.getBlockIdByNum(-1); - Assert.fail(); - } catch (ItemNotFoundException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(ItemNotFoundException.class, + () -> chainManager.getBlockIdByNum(-1)); try { dbManager.getBlockChainHashesOnFork(blockCapsule2.getBlockId()); } catch (Exception e) { @@ -1185,14 +1181,8 @@ public void testExpireTransaction() { TransactionCapsule trx = new TransactionCapsule(tc, ContractType.TransferContract); long latestBlockTime = dbManager.getDynamicPropertiesStore().getLatestBlockHeaderTimestamp(); trx.setExpiration(latestBlockTime - 100); - try { - dbManager.validateCommon(trx); - Assert.fail(); - } catch (TransactionExpirationException e) { - Assert.assertTrue(true); - } catch (TooBigTransactionException e) { - Assert.fail(); - } + Assert.assertThrows(TransactionExpirationException.class, + () -> dbManager.validateCommon(trx)); } @Test diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java index 76e9a18e31a..3ee61065d1f 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java @@ -38,7 +38,7 @@ protected void beforeDestroy() { * from: get key1 or key2, traverse 0 times */ @Test - public void testMergeRoot() { + public void testMergeRoot() throws Exception { // linklist is: from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); //root.setOptimized(true); @@ -68,7 +68,7 @@ public void testMergeRoot() { * */ @Test - public void testMergeAhead() { + public void testMergeAhead() throws Exception { // linklist is: from2 -> from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); @@ -136,7 +136,7 @@ public void testMergeAhead() { * from2: key1=>value1, key2=>value2, key3=>value32, key4=>value4 */ @Test - public void testMergeOverride() { + public void testMergeOverride() throws Exception { // linklist is: from2 -> from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); SnapshotImpl from = getSnapshotImplIns(root); @@ -165,16 +165,11 @@ public void testMergeOverride() { * The constructor of SnapshotImpl is not public * so reflection is used to construct the object here. */ - private SnapshotImpl getSnapshotImplIns(Snapshot snapshot) { + private SnapshotImpl getSnapshotImplIns(Snapshot snapshot) throws Exception { Class clazz = SnapshotImpl.class; - try { - Constructor constructor = clazz.getDeclaredConstructor(Snapshot.class); - constructor.setAccessible(true); - return (SnapshotImpl) constructor.newInstance(snapshot); - } catch (Exception e) { - e.printStackTrace(); - } - return null; + Constructor constructor = clazz.getDeclaredConstructor(Snapshot.class); + constructor.setAccessible(true); + return (SnapshotImpl) constructor.newInstance(snapshot); } } diff --git a/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java b/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java index 82c887fad53..6c0b8733a18 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java @@ -20,21 +20,15 @@ public void test() throws Exception { be1.setBlockId(b1); be1.setParentId(b1); be1.setSolidId(b1); - try { - BlockEventCache.add(be1); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof EventException); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> BlockEventCache.add(be1)); + Assert.assertTrue(e1 instanceof EventException); BlockEventCache.init(new BlockCapsule.BlockId(getBlockId(), 100)); - try { - BlockEventCache.add(be1); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof EventException); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> BlockEventCache.add(be1)); + Assert.assertTrue(e2 instanceof EventException); BlockEventCache.init(b1); diff --git a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java index 6f0f601827c..e2815e46063 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java @@ -34,7 +34,6 @@ import org.tron.core.ChainBaseManager; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; -import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.capsule.WitnessCapsule; import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @@ -45,7 +44,7 @@ import org.tron.core.services.event.BlockEventGet; import org.tron.core.services.event.bo.BlockEvent; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.core.store.TransactionRetStore; +import org.tron.core.vm.config.ConfigLoader; import org.tron.protos.Protocol; @Slf4j @@ -69,8 +68,8 @@ public class BlockEventGetTest extends BlockGenerate { static LocalDateTime localDateTime = LocalDateTime.now(); - private long time = ZonedDateTime.of(localDateTime, - ZoneId.systemDefault()).toInstant().toEpochMilli(); + private long time = + ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).toInstant().toEpochMilli(); public static String dbPath() { @@ -100,18 +99,24 @@ public void before() throws IOException { chainManager = dbManager.getChainBaseManager(); tronNetDelegate = context.getBean(TronNetDelegate.class); tronNetDelegate.setExit(false); - currentHeader = dbManager.getDynamicPropertiesStore() - .getLatestBlockHeaderNumberFromDB(); + currentHeader = dbManager.getDynamicPropertiesStore().getLatestBlockHeaderNumberFromDB(); ByteString addressBS = ByteString.copyFrom(address); WitnessCapsule witnessCapsule = new WitnessCapsule(addressBS); chainManager.getWitnessStore().put(address, witnessCapsule); chainManager.addWitness(addressBS); - AccountCapsule accountCapsule = new AccountCapsule(Protocol.Account.newBuilder() - .setAddress(addressBS).setBalance((long) 1e10).build()); + AccountCapsule accountCapsule = new AccountCapsule( + Protocol.Account.newBuilder().setAddress(addressBS).setBalance((long) 1e10).build()); chainManager.getAccountStore().put(address, accountCapsule); + // Reset global static flag that other tests may leave as true, which would prevent + // ConfigLoader.load() from updating VMConfig during VMActuator.execute(). + ConfigLoader.disable = false; + // Reset filterQuery so FilterQueryTest's leftover state does not suppress processTrigger + // coverage when tests share the same Gradle forkEvery JVM batch. + EventPluginLoader.getInstance().setFilterQuery(null); + DynamicPropertiesStore dps = dbManager.getDynamicPropertiesStore(); dps.saveAllowTvmTransferTrc10(1); dps.saveAllowTvmConstantinople(1); @@ -130,8 +135,8 @@ public void test() throws Exception { Manager manager = context.getBean(Manager.class); WitnessCapsule witnessCapsule = new WitnessCapsule(ByteString.copyFrom(address)); - ChainBaseManager.getChainBaseManager() - .getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + ChainBaseManager.getChainBaseManager().getWitnessScheduleStore() + .saveActiveWitnesses(new ArrayList<>()); ChainBaseManager.getChainBaseManager().addWitness(ByteString.copyFrom(address)); String code = "608060405234801561000f575f80fd5b50d3801561001b575f80fd5b50d28015610027575f" @@ -142,15 +147,16 @@ public void test() throws Exception { + "00a0565b9050919050565b6100dc816100b2565b82525050565b5f6020820190506100f55f83018461" + "00d3565b92915050565b603e806101075f395ff3fe60806040525f80fdfea26474726f6e582212200c" + "57c973388f044038eff0e6474425b38037e75e66d6b3047647290605449c7764736f6c63430008140033"; - Protocol.Transaction trx = TvmTestUtils.generateDeploySmartContractAndGetTransaction( - "TestTRC20", address, "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\"" - + ":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\"" - + ":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\"" - + ":\"Transfer\",\"type\":\"event\"}]", code, 0, (long) 1e9, 100, null, 1); - trx = trx.toBuilder().addRet( - Protocol.Transaction.Result.newBuilder() - .setContractRetValue(Protocol.Transaction.Result.contractResult.SUCCESS_VALUE) - .build()).build(); + Protocol.Transaction trx = + TvmTestUtils.generateDeploySmartContractAndGetTransaction("TestTRC20", address, + "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\"" + + ":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\"" + + ":\"address\"},{\"indexed\":false,\"name\":\"value\"," + + "\"type\":\"uint256\"}],\"name\"" + + ":\"Transfer\",\"type\":\"event\"}]", code, 0, (long) 1e9, 100, null, 1); + trx = trx.toBuilder().addRet(Protocol.Transaction.Result.newBuilder() + .setContractRetValue(Protocol.Transaction.Result.contractResult.SUCCESS_VALUE).build()) + .build(); Protocol.Block block = getSignedBlock(witnessCapsule.getAddress(), time, privateKey); BlockCapsule blockCapsule = new BlockCapsule(block.toBuilder().addTransactions(trx).build()); @@ -164,8 +170,7 @@ public void test() throws Exception { // Set energy price history to test boundary cases manager.getDynamicPropertiesStore().saveEnergyPriceHistory( - manager.getDynamicPropertiesStore().getEnergyPriceHistory() - + "," + time + ":210"); + manager.getDynamicPropertiesStore().getEnergyPriceHistory() + "," + time + ":210"); EventPluginConfig config = new EventPluginConfig(); config.setSendQueueLength(1000); @@ -208,8 +213,9 @@ public void test() throws Exception { // Here energy unit price should be 100 not 210, // cause block time is equal to 210`s effective time - Assert.assertEquals(100, blockEvent.getTransactionLogTriggerCapsules() - .get(0).getTransactionLogTrigger().getEnergyUnitPrice()); + Assert.assertEquals(100, + blockEvent.getTransactionLogTriggerCapsules().get(0).getTransactionLogTrigger() + .getEnergyUnitPrice()); } catch (Exception e) { Assert.fail(); } @@ -218,8 +224,8 @@ public void test() throws Exception { @Test public void getTransactionTriggers() throws Exception { BlockEventGet blockEventGet = new BlockEventGet(); - BlockCapsule bc = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - 100, Sha256Hash.ZERO_HASH.getByteString()); + BlockCapsule bc = + new BlockCapsule(1, Sha256Hash.ZERO_HASH, 100, Sha256Hash.ZERO_HASH.getByteString()); List list = blockEventGet.getTransactionTriggers(bc, 1); Assert.assertEquals(0, list.size()); @@ -238,7 +244,7 @@ public void getTransactionTriggers() throws Exception { Manager manager = mock(Manager.class); ReflectUtils.setFieldValue(blockEventGet, "manager", manager); Mockito.when(manager.getTransactionInfoByBlockNum(1)) - .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); + .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); list = blockEventGet.getTransactionTriggers(bc, 1); Assert.assertEquals(1, list.size()); @@ -255,8 +261,7 @@ public void getTransactionTriggers() throws Exception { resourceBuild.setNetUsage(6); String address = "A0B4750E2CD76E19DCA331BF5D089B71C3C2798548"; - infoBuild - .setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(address))) + infoBuild.setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(address))) .addContractResult(ByteString.copyFrom(ByteArray.fromHexString("112233"))) .setReceipt(resourceBuild.build()); @@ -264,14 +269,12 @@ public void getTransactionTriggers() throws Exception { Mockito.when(manager.getChainBaseManager()).thenReturn(chainBaseManager); - GrpcAPI.TransactionInfoList result = GrpcAPI.TransactionInfoList.newBuilder() - .addTransactionInfo(infoBuild.build()).build(); + GrpcAPI.TransactionInfoList result = + GrpcAPI.TransactionInfoList.newBuilder().addTransactionInfo(infoBuild.build()).build(); - Mockito.when(manager.getTransactionInfoByBlockNum(0)) - .thenReturn(result); + Mockito.when(manager.getTransactionInfoByBlockNum(0)).thenReturn(result); - Protocol.Block block = Protocol.Block.newBuilder() - .addTransactions(transaction).build(); + Protocol.Block block = Protocol.Block.newBuilder().addTransactions(transaction).build(); BlockCapsule blockCapsule = new BlockCapsule(block); blockCapsule.getTransactions().forEach(t -> t.setBlockNum(blockCapsule.getNum())); @@ -279,23 +282,17 @@ public void getTransactionTriggers() throws Exception { list = blockEventGet.getTransactionTriggers(blockCapsule, 1); Assert.assertEquals(1, list.size()); - Assert.assertEquals(1, - list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getBlockNumber()); - Assert.assertEquals(2, - list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); + Assert.assertEquals(1, list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getBlockNumber()); + Assert.assertEquals(2, list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); Mockito.when(manager.getTransactionInfoByBlockNum(0)) .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); list = blockEventGet.getTransactionTriggers(blockCapsule, 1); Assert.assertEquals(1, list.size()); - Assert.assertEquals(1, - list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); + Assert.assertEquals(1, list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); } } \ No newline at end of file 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 abb73671161..2cdcaaf7a53 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java @@ -7,9 +7,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.logsfilter.capsule.BlockFilterCapsule; import org.tron.common.utils.ByteArray; import org.tron.core.exception.ItemNotFoundException; @@ -18,6 +22,7 @@ @Slf4j public class ConcurrentHashMapTest { + private static final String EXECUTOR_NAME = "jsonrpc-concurrent-map-test"; private static int randomInt(int minInt, int maxInt) { return (int) round(random(true) * (maxInt - minInt) + minInt, true); @@ -52,12 +57,14 @@ public void testHandleBlockHash() { try { Thread.sleep(200); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + Assert.fail("Interrupted during test setup: " + e.getMessage()); } - Thread putThread = new Thread(new Runnable() { - public void run() { + ExecutorService executor = ExecutorServiceManager.newFixedThreadPool(EXECUTOR_NAME, 4, true); + try { + Future putTask = executor.submit(() -> { for (int i = 1; i <= times; i++) { logger.info("put time {}, from {} to {}", i, (1 + (i - 1) * eachCount), i * eachCount); @@ -69,21 +76,20 @@ public void run() { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("putThread interrupted", e); } } - } - - }); + }); - Thread getThread1 = new Thread(new Runnable() { - public void run() { + Future getTask1 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread1 interrupted", e); } logger.info("Thread1 get time {}", t); @@ -98,22 +104,20 @@ public void run() { } } catch (ItemNotFoundException e) { - e.printStackTrace(); - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } - } - }); + }); - Thread getThread2 = new Thread(new Runnable() { - public void run() { + Future getTask2 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread2 interrupted", e); } logger.info("Thread2 get time {}", t); @@ -132,21 +136,20 @@ public void run() { } } catch (ItemNotFoundException e) { - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } - } - }); + }); - Thread getThread3 = new Thread(new Runnable() { - public void run() { + Future getTask3 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread3 interrupted", e); } logger.info("Thread3 get time {}", t); @@ -160,31 +163,30 @@ public void run() { try { resultMap3.get(String.valueOf(k)).add(str.toString()); } catch (Exception e) { - logger.error("resultMap3 get {} exception {}", k, e.getMessage()); - e.printStackTrace(); + throw new AssertionError("resultMap3 get " + k + " exception", e); } } } catch (ItemNotFoundException e) { - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } + }); + + for (Future future : new Future[] {putTask, getTask1, getTask2, getTask3}) { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Assert.fail("Main thread interrupted while waiting for worker threads: " + + e.getMessage()); + } catch (ExecutionException e) { + Assert.fail("Worker thread failed: " + e.getCause()); + } } - }); - - putThread.start(); - getThread1.start(); - getThread2.start(); - getThread3.start(); - - try { - putThread.join(); - getThread1.join(); - getThread2.join(); - getThread3.join(); - } catch (InterruptedException e) { - e.printStackTrace(); + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(executor, EXECUTOR_NAME); } logger.info("-----------------------------------------------------------------------"); @@ -205,4 +207,4 @@ public void run() { } } -} \ No newline at end of file +} 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 ced7048c9d2..e1a698216c4 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -345,20 +345,14 @@ public void testGetBlockByNumber() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule2.getNum()), blockResult.getNumber()); // pending - try { - tronJsonRpc.ethGetBlockByNumber("pending", false); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("pending", false)); + Assert.assertEquals("TAG pending not supported", e1.getMessage()); // invalid - try { - tronJsonRpc.ethGetBlockByNumber("0x", false); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("0x", false)); + Assert.assertEquals("invalid block number", e2.getMessage()); } @Test @@ -432,12 +426,9 @@ public void testServicesInit() { public void testGetByJsonBlockId() { long blkNum = 0; - try { - getByJsonBlockId("pending", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("pending", wallet)); + Assert.assertEquals("TAG pending not supported", pendingEx.getMessage()); try { blkNum = getByJsonBlockId(null, wallet); @@ -467,49 +458,34 @@ public void testGetByJsonBlockId() { } Assert.assertEquals(10L, blkNum); - try { - getByJsonBlockId("abc", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("Incorrect hex syntax", e.getMessage()); - } + Exception abcEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("abc", wallet)); + Assert.assertEquals("Incorrect hex syntax", abcEx.getMessage()); - try { - getByJsonBlockId("0xxabc", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed - Assert.assertTrue(e.getMessage().startsWith("For input string: \"xabc\"")); - } + Exception hexEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("0xxabc", wallet)); + // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed + Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); } @Test public void testGetTrxBalance() { String balance = ""; - try { - tronJsonRpc.getTrxBalance("", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); - try { - tronJsonRpc.getTrxBalance("", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); - try { - tronJsonRpc.getTrxBalance("", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); try { balance = tronJsonRpc.getTrxBalance("0xabd4b9367799eaa3197fecb144eb71de1e049abc", @@ -522,83 +498,56 @@ public void testGetTrxBalance() { @Test public void testGetStorageAt() { - try { - tronJsonRpc.getStorageAt("", "", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getStorageAt("", "", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getStorageAt("", "", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } @Test public void testGetABIOfSmartContract() { - try { - tronJsonRpc.getABIOfSmartContract("", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getABIOfSmartContract("", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getABIOfSmartContract("", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } @Test public void testGetCall() { - try { - tronJsonRpc.getCall(null, "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getCall(null, "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getCall(null, "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } /** @@ -666,13 +615,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x78", "0x14", - null, null, null), 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); - } + JsonRpcInvalidParamsException fromToEx = + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x78", "0x14", + null, null, null), 100, null, false)); + Assert.assertEquals("please verify: fromBlock <= toBlock", fromToEx.getMessage()); //fromBlock or toBlock is not hex num try { @@ -691,13 +638,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("pending", null, null, null, null), - 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + JsonRpcInvalidParamsException pendingFilterEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("pending", null, null, null, null), + 100, null, false)); + Assert.assertEquals("TAG pending not supported", pendingFilterEx.getMessage()); try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper(new FilterRequest("finalized", null, null, null, null), 100, wallet, false); @@ -706,13 +651,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("test", null, null, null, null), - 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("Incorrect hex syntax", e.getMessage()); - } + JsonRpcInvalidParamsException testSyntaxEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("test", null, null, null, null), + 100, null, false)); + Assert.assertEquals("Incorrect hex syntax", testSyntaxEx.getMessage()); // to = 8000 try { @@ -722,15 +665,13 @@ public void testLogFilterWrapper() { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx1 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx1.getMessage()); try { new LogFilterWrapper(new FilterRequest("0x0", "latest", null, @@ -739,15 +680,13 @@ public void testLogFilterWrapper() { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x0", "latest", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx2 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x0", "latest", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx2.getMessage()); // from = 100, current = 5_000, to = Long.MAX_VALUE try { @@ -764,15 +703,13 @@ public void testLogFilterWrapper() { } // from = 100 - try { - new LogFilterWrapper(new FilterRequest("0x64", "latest", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx3 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x64", "latest", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx3.getMessage()); try { new LogFilterWrapper(new FilterRequest("0x64", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); @@ -866,15 +803,13 @@ public void testMaxSubTopics() { } topics.add(subTopics); - try { - new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", - null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), - e.getMessage()); - } + JsonRpcInvalidParamsException topicsEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", + null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false)); + Assert.assertEquals( + "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), + topicsEx.getMessage()); try { tronJsonRpc.getLogs(new FilterRequest("0xbb8", "0x1f40", @@ -978,40 +913,25 @@ public void testNewFilterFinalizedBlock() { Assert.fail(); } - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", null, null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", null, null, null, null))); + Assert.assertEquals("invalid block range params", e1.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest(null, "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest(null, "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e2.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", "latest", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "latest", null, null, null))); + Assert.assertEquals("invalid block range params", e3.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e4.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e5.getMessage()); } @Test @@ -1052,19 +972,13 @@ public void testGetBlockReceipts() { throw new RuntimeException(e); } - try { - tronJsonRpc.getBlockReceipts("pending"); - Assert.fail(); - } catch (Exception e) { - Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); - } + Exception pendingReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("pending")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingReceiptsEx.getMessage()); - try { - tronJsonRpc.getBlockReceipts("test"); - Assert.fail(); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception testReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("test")); + Assert.assertEquals("invalid block number", testReceiptsEx.getMessage()); try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java index d80d10694a8..94269e86fec 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java @@ -3,7 +3,6 @@ import java.lang.reflect.Method; import java.util.BitSet; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Resource; import org.junit.After; import org.junit.Assert; @@ -11,6 +10,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.es.ExecutorServiceManager; import org.tron.core.config.args.Args; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.filters.LogBlockQuery; @@ -21,6 +21,7 @@ public class LogBlockQueryTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + private static final String EXECUTOR_NAME = "log-block-query-test"; private ExecutorService sectionExecutor; private Method partialMatchMethod; private static final long CURRENT_MAX_BLOCK_NUM = 50000L; @@ -31,10 +32,10 @@ public class LogBlockQueryTest extends BaseTest { @Before public void setup() throws Exception { - sectionExecutor = Executors.newFixedThreadPool(5); - + sectionExecutor = ExecutorServiceManager.newFixedThreadPool(EXECUTOR_NAME, 5); + // Get private method through reflection - partialMatchMethod = LogBlockQuery.class.getDeclaredMethod("partialMatch", + partialMatchMethod = LogBlockQuery.class.getDeclaredMethod("partialMatch", int[][].class, int.class); partialMatchMethod.setAccessible(true); @@ -52,9 +53,7 @@ public void setup() throws Exception { @After public void tearDown() { - if (sectionExecutor != null && !sectionExecutor.isShutdown()) { - sectionExecutor.shutdown(); - } + ExecutorServiceManager.shutdownAndAwaitTermination(sectionExecutor, EXECUTOR_NAME); } @Test @@ -63,8 +62,8 @@ public void testPartialMatch() throws Exception { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("0x0", "0x1", null, null, null), CURRENT_MAX_BLOCK_NUM, null, false); - - LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, + + LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, CURRENT_MAX_BLOCK_NUM, sectionExecutor); int section = 0; @@ -105,4 +104,4 @@ public void testPartialMatch() throws Exception { Assert.assertNotNull(result); Assert.assertTrue(result.isEmpty()); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java index d31f7a4f63d..39bcc30e278 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java @@ -4,12 +4,14 @@ import java.util.BitSet; import java.util.List; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Resource; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.ByteArray; @@ -28,10 +30,22 @@ public class SectionBloomStoreTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + private ExecutorService sectionExecutor; + static { Args.setParam(new String[] {"--output-directory", dbPath()}, TestConstants.TEST_CONF); } + @Before + public void setUp() { + sectionExecutor = ExecutorServiceManager.newFixedThreadPool("section-bloom-query", 5); + } + + @After + public void tearDown() { + ExecutorServiceManager.shutdownAndAwaitTermination(sectionExecutor, "section-bloom-query"); + } + @Test public void testPutAndGet() { BitSet bitSet = new BitSet(SectionBloomStore.BLOCK_PER_SECTION); @@ -126,7 +140,6 @@ public void testWriteAndQuery() { } long currentMaxBlockNum = 50000; - ExecutorService sectionExecutor = Executors.newFixedThreadPool(5); //query one address try { @@ -236,6 +249,5 @@ public void testWriteAndQuery() { Assert.fail(); } - sectionExecutor.shutdownNow(); } } diff --git a/framework/src/test/java/org/tron/core/net/MessageTest.java b/framework/src/test/java/org/tron/core/net/MessageTest.java index 5b81d18a599..3757333aa6d 100644 --- a/framework/src/test/java/org/tron/core/net/MessageTest.java +++ b/framework/src/test/java/org/tron/core/net/MessageTest.java @@ -19,29 +19,16 @@ public class MessageTest { private DisconnectMessage disconnectMessage; @Test - public void test1() throws Exception { - byte[] bytes = new DisconnectMessage(ReasonCode.TOO_MANY_PEERS).getData(); + public void test1() { DisconnectMessageTest disconnectMessageTest = new DisconnectMessageTest(); try { disconnectMessage = new DisconnectMessage(MessageTypes.P2P_DISCONNECT.asByte(), disconnectMessageTest.toByteArray()); } catch (Exception e) { - System.out.println(e.getMessage()); Assert.assertTrue(e instanceof P2pException); } } - public void test2() throws Exception { - DisconnectMessageTest disconnectMessageTest = new DisconnectMessageTest(); - long startTime = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - disconnectMessage = new DisconnectMessage(MessageTypes.P2P_DISCONNECT.asByte(), - disconnectMessageTest.toByteArray()); - } - long endTime = System.currentTimeMillis(); - System.out.println("spend time : " + (endTime - startTime)); - } - @Test public void testMessageStatistics() { MessageStatistics messageStatistics = new MessageStatistics(); diff --git a/framework/src/test/java/org/tron/core/net/NodeTest.java b/framework/src/test/java/org/tron/core/net/NodeTest.java index cbf545af646..979c306fd98 100644 --- a/framework/src/test/java/org/tron/core/net/NodeTest.java +++ b/framework/src/test/java/org/tron/core/net/NodeTest.java @@ -28,30 +28,18 @@ public class NodeTest { public void testIpV4() { InetSocketAddress address1 = NetUtil.parseInetSocketAddress("192.168.0.1:18888"); Assert.assertNotNull(address1); - try { - NetUtil.parseInetSocketAddress("192.168.0.1"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("192.168.0.1")); } @Test public void testIpV6() { - try { - NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb:18888"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb:18888")); InetSocketAddress address2 = NetUtil.parseInetSocketAddress("[fe80::216:3eff:fe0e:23bb]:18888"); Assert.assertNotNull(address2); - try { - NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb")); } @Test diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java index 270002fffba..e05ee29d015 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java @@ -111,21 +111,17 @@ public void testSyncFetchCheck() { FetchInvDataMsgHandler fetchInvDataMsgHandler = new FetchInvDataMsgHandler(); - try { - Mockito.when(peer.getLastSyncBlockId()) + Mockito.when(peer.getLastSyncBlockId()) .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 1000L)); - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals(e.getMessage(), "maxBlockNum: 1000, blockNum: 10000"); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("maxBlockNum: 1000, blockNum: 10000", e1.getMessage()); - try { - Mockito.when(peer.getLastSyncBlockId()) + Mockito.when(peer.getLastSyncBlockId()) .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 20000L)); - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals(e.getMessage(), "minBlockNum: 16000, blockNum: 10000"); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("minBlockNum: 16000, blockNum: 10000", e2.getMessage()); } @Test @@ -148,15 +144,12 @@ public void testRateLimiter() { Mockito.when(peer.getP2pRateLimiter()).thenReturn(p2pRateLimiter); FetchInvDataMsgHandler fetchInvDataMsgHandler = new FetchInvDataMsgHandler(); - try { - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals("fetch too many blocks, size:101", e.getMessage()); - } - try { - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertTrue(e.getMessage().endsWith("rate limit")); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("fetch too many blocks, size:101", e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertTrue(e2.getMessage().endsWith("rate limit")); } } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java index 338b44e6699..1dbf7c7150f 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java @@ -49,6 +49,7 @@ public void testProcessMessage() throws Exception { field.set(handler, tronNetDelegate); handler.processMessage(peer, msg); + Mockito.verify(tronNetDelegate, Mockito.atLeastOnce()).isBlockUnsolidified(); } private Channel getChannel(String host, int port) throws Exception { diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java index b1fb197a2e9..be843674632 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java @@ -13,13 +13,13 @@ import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.Sha256Hash; import org.tron.consensus.pbft.message.PbftMessage; import org.tron.core.capsule.BlockCapsule; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.net.P2pEventHandlerImpl; import org.tron.core.net.TronNetService; @@ -33,6 +33,8 @@ public class MessageHandlerTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private PeerConnection peer; private static P2pEventHandlerImpl p2pEventHandler; private static ApplicationContext ctx; @@ -45,7 +47,7 @@ public class MessageHandlerTest { public static void init() throws Exception { Args.setParam(new String[] {"--output-directory", temporaryFolder.newFolder().toString(), "--debug"}, TestConstants.TEST_CONF); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); p2pEventHandler = context.getBean(P2pEventHandlerImpl.class); ctx = (ApplicationContext) ReflectUtils.getFieldObject(p2pEventHandler, "ctx"); @@ -57,7 +59,7 @@ public static void init() throws Exception { @AfterClass public static void destroy() { Args.clearParam(); - context.destroy(); + APP_FIXTURE.close(); } @After diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java index 649e5eb0875..cc30fb70b0b 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java @@ -12,6 +12,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; +import org.tron.common.TestConstants; import org.tron.common.overlay.message.Message; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Pair; @@ -32,6 +33,7 @@ public class PeerConnectionTest { @BeforeClass public static void initArgs() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); CommonParameter.getInstance().setRateLimiterSyncBlockChain(10); CommonParameter.getInstance().setRateLimiterFetchInvData(10); CommonParameter.getInstance().setRateLimiterDisconnect(10); @@ -203,6 +205,8 @@ public void testSetChannel() { relayNodes.add(inetSocketAddress); peerConnection.setChannel(c1); Assert.assertTrue(peerConnection.isRelayPeer()); + + ReflectUtils.setFieldValue(peerConnection, "relayNodes", new ArrayList<>()); } @Test @@ -234,15 +238,12 @@ public void testCheckAndPutAdvInvRequest() { @Test public void testEquals() { - List relayNodes = new ArrayList<>(); - PeerConnection p1 = new PeerConnection(); InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.2", 10001); Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); p1.setChannel(c1); PeerConnection p2 = new PeerConnection(); @@ -251,7 +252,6 @@ public void testEquals() { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); p2.setChannel(c2); PeerConnection p3 = new PeerConnection(); @@ -260,7 +260,6 @@ public void testEquals() { Channel c3 = new Channel(); ReflectUtils.setFieldValue(c3, "inetSocketAddress", inetSocketAddress3); ReflectUtils.setFieldValue(c3, "inetAddress", inetSocketAddress3.getAddress()); - ReflectUtils.setFieldValue(p3, "relayNodes", relayNodes); p3.setChannel(c3); Assert.assertTrue(p1.equals(p1)); @@ -270,15 +269,12 @@ public void testEquals() { @Test public void testHashCode() { - List relayNodes = new ArrayList<>(); - PeerConnection p1 = new PeerConnection(); InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.2", 10001); Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); p1.setChannel(c1); PeerConnection p2 = new PeerConnection(); @@ -287,7 +283,6 @@ public void testHashCode() { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); p2.setChannel(c2); PeerConnection p3 = new PeerConnection(); @@ -296,7 +291,6 @@ public void testHashCode() { Channel c3 = new Channel(); ReflectUtils.setFieldValue(c3, "inetSocketAddress", inetSocketAddress3); ReflectUtils.setFieldValue(c3, "inetAddress", inetSocketAddress3.getAddress()); - ReflectUtils.setFieldValue(p3, "relayNodes", relayNodes); p3.setChannel(c3); Assert.assertTrue(p1.hashCode() != p2.hashCode()); diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java index ee409a8ab04..ffba127a6fd 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java @@ -15,16 +15,17 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; +import org.tron.common.TestConstants; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ReflectUtils; import org.tron.core.config.args.Args; import org.tron.p2p.connection.Channel; public class PeerManagerTest { - List relayNodes = new ArrayList<>(); @BeforeClass public static void initArgs() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); CommonParameter.getInstance().setRateLimiterSyncBlockChain(10); CommonParameter.getInstance().setRateLimiterFetchInvData(10); CommonParameter.getInstance().setRateLimiterDisconnect(10); @@ -54,7 +55,7 @@ public void testAdd() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -79,7 +80,7 @@ public void testRemove() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -105,7 +106,7 @@ public void testGetPeerConnection() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -128,7 +129,7 @@ public void testGetPeers() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -146,7 +147,7 @@ public void testGetPeers() throws Exception { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); + p2.setChannel(c2); ApplicationContext ctx2 = mock(ApplicationContext.class); diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java index 80b1abdc35d..d2ee4be5b87 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java @@ -1,11 +1,17 @@ package org.tron.core.net.peer; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; import org.mockito.Mockito; +import org.tron.common.utils.ReflectUtils; public class PeerStatusCheckMockTest { @After @@ -14,13 +20,25 @@ public void clearMocks() { } @Test - public void testInitException() throws InterruptedException { + public void testInitException() { PeerStatusCheck peerStatusCheck = spy(new PeerStatusCheck()); + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + ReflectUtils.setFieldValue(peerStatusCheck, "peerStatusCheckExecutor", executor); doThrow(new RuntimeException("test exception")).when(peerStatusCheck).statusCheck(); + peerStatusCheck.init(); - // the initialDelay of scheduleWithFixedDelay is 5s - Thread.sleep(5000L); + Mockito.verify(executor).scheduleWithFixedDelay(any(Runnable.class), eq(5L), eq(2L), + eq(TimeUnit.SECONDS)); + Runnable scheduledTask = Mockito.mockingDetails(executor).getInvocations().stream() + .filter(invocation -> invocation.getMethod().getName().equals("scheduleWithFixedDelay")) + .map(invocation -> (Runnable) invocation.getArgument(0)) + .findFirst() + .orElseThrow(() -> new AssertionError("scheduled task was not registered")); + + scheduledTask.run(); + + Mockito.verify(peerStatusCheck).statusCheck(); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java index 6f34288939f..8585244b941 100644 --- a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java @@ -89,7 +89,6 @@ private void initWitness() { // Base58: TNboetpFgv9SqMoHvaVt626NLXETnbdW1K byte[] key = Hex.decode("418A8D690BF36806C36A7DAE3AF796643C1AA9CC01");//exist already WitnessCapsule witnessCapsule = chainBaseManager.getWitnessStore().get(key); - System.out.println(witnessCapsule.getInstance()); witnessCapsule.setVoteCount(1000); chainBaseManager.getWitnessStore().put(key, witnessCapsule); List list = new ArrayList<>(); diff --git a/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java b/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java index a940a14d392..7ad56c464bb 100644 --- a/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java +++ b/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java @@ -7,8 +7,10 @@ import org.junit.Assert; import org.junit.Test; +import org.tron.core.net.TronNetService; import org.tron.core.net.service.statistics.NodeStatistics; import org.tron.core.net.service.statistics.TronStatsManager; +import org.tron.p2p.stats.P2pStats; import org.tron.protos.Protocol; public class TronStatsManagerTest { @@ -50,14 +52,20 @@ public void testWork() throws Exception { Assert.assertEquals(field3.get(manager), 1L); Assert.assertEquals(field4.get(manager), 1L); + P2pStats statsSnapshot = TronNetService.getP2pService().getP2pStats(); + long expectedTcpIn = statsSnapshot.getTcpInSize(); + long expectedTcpOut = statsSnapshot.getTcpOutSize(); + long expectedUdpIn = statsSnapshot.getUdpInSize(); + long expectedUdpOut = statsSnapshot.getUdpOutSize(); + Method method = manager.getClass().getDeclaredMethod("work"); method.setAccessible(true); method.invoke(manager); - Assert.assertEquals(field1.get(manager), 0L); - Assert.assertEquals(field2.get(manager), 0L); - Assert.assertEquals(field3.get(manager), 0L); - Assert.assertEquals(field4.get(manager), 0L); + Assert.assertEquals(expectedTcpIn, (long) field1.get(manager)); + Assert.assertEquals(expectedTcpOut, (long) field2.get(manager)); + Assert.assertEquals(expectedUdpIn, (long) field3.get(manager)); + Assert.assertEquals(expectedUdpOut, (long) field4.get(manager)); } } diff --git a/framework/src/test/java/org/tron/core/pbft/PbftTest.java b/framework/src/test/java/org/tron/core/pbft/PbftTest.java index 33a46516988..10965240c8e 100644 --- a/framework/src/test/java/org/tron/core/pbft/PbftTest.java +++ b/framework/src/test/java/org/tron/core/pbft/PbftTest.java @@ -30,7 +30,7 @@ public void testPbftSrMessage() { PbftMessage pbftSrMessage = PbftMessage .prePrepareSRLMsg(blockCapsule, srList, 1, miner); PbftMessage.fullNodePrePrepareSRLMsg(blockCapsule, srList, 1); - System.out.println(pbftSrMessage); + org.junit.Assert.assertNotNull(pbftSrMessage); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java b/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java index 7617af2c1eb..a1bffd5bf1f 100644 --- a/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java +++ b/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java @@ -12,7 +12,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.Timeout; import org.tron.common.BaseMethodTest; import org.tron.common.error.TronDBException; import org.tron.common.es.ExecutorServiceManager; @@ -33,6 +35,11 @@ public class ComputeRewardTest extends BaseMethodTest { + // setUp() contains a 6-second sleep waiting for async reward calculation; + // 60 s total budget covers setup + test body with headroom for slow CI. + @Rule + public Timeout timeout = Timeout.seconds(60); + private static final byte[] OWNER_ADDRESS = ByteArray.fromHexString( "4105b9e8af8ee371cad87317f442d155b39fbd1bf0"); diff --git a/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java b/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java index fc60c2afa03..a16a71c4e59 100644 --- a/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java @@ -78,7 +78,6 @@ private void testWithdraw() { mortgageService.withdrawReward(sr1); accountCapsule = dbManager.getAccountStore().get(sr1); allowance = accountCapsule.getAllowance() - allowance; - System.out.println("withdrawReward:" + allowance); Assert.assertEquals(reward, allowance); } diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index f40ec48e035..94b05cae98f 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -51,9 +51,8 @@ import org.tron.api.WalletGrpc.WalletBlockingStub; import org.tron.api.WalletSolidityGrpc; import org.tron.api.WalletSolidityGrpc.WalletSolidityBlockingStub; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ByteArray; @@ -65,7 +64,6 @@ import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.db.Manager; import org.tron.protos.Protocol; @@ -118,8 +116,9 @@ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class RpcApiServicesTest { - private static Application appTest; private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelPBFT = null; private static ManagedChannel channelSolidity = null; @@ -188,7 +187,7 @@ public static void init() throws IOException { .executor(executorService) .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); databaseBlockingStubFull = DatabaseGrpc.newBlockingStub(channelFull); databaseBlockingStubSolidity = DatabaseGrpc.newBlockingStub(channelSolidity); databaseBlockingStubPBFT = DatabaseGrpc.newBlockingStub(channelPBFT); @@ -204,15 +203,12 @@ public static void init() throws IOException { manager.getAccountStore().put(ownerCapsule.createDbKey(), ownerCapsule); manager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); manager.getDynamicPropertiesStore().saveAllowShieldedTRC20Transaction(1); - appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } @AfterClass public static void destroy() { - shutdownChannel(channelFull); - shutdownChannel(channelPBFT); - shutdownChannel(channelSolidity); + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelPBFT, channelSolidity); if (executorService != null) { ExecutorServiceManager.shutdownAndAwaitTermination( @@ -220,25 +216,10 @@ public static void destroy() { executorService = null; } - context.close(); + APP_FIXTURE.close(); Args.clearParam(); } - private static void shutdownChannel(ManagedChannel channel) { - if (channel == null) { - return; - } - try { - channel.shutdown(); - if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { - channel.shutdownNow(); - } - } catch (InterruptedException e) { - channel.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - @Test public void testGetBlockByNum() { NumberMessage message = NumberMessage.newBuilder().setNum(0).build(); diff --git a/framework/src/test/java/org/tron/core/services/WalletApiTest.java b/framework/src/test/java/org/tron/core/services/WalletApiTest.java index b7a26d6dc73..4a55556afb1 100644 --- a/framework/src/test/java/org/tron/core/services/WalletApiTest.java +++ b/framework/src/test/java/org/tron/core/services/WalletApiTest.java @@ -14,13 +14,11 @@ import org.junit.rules.Timeout; import org.tron.api.GrpcAPI.EmptyMessage; import org.tron.api.WalletGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @@ -34,7 +32,8 @@ public class WalletApiTest { public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); private static TronApplicationContext context; - private static Application appT; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); @BeforeClass @@ -43,9 +42,7 @@ public static void init() throws IOException { "--p2p-disable", "true"}, TestConstants.TEST_CONF); Args.getInstance().setRpcPort(PublicMethod.chooseRandomPort()); Args.getInstance().setRpcEnable(true); - context = new TronApplicationContext(DefaultConfig.class); - appT = ApplicationFactory.create(context); - appT.startup(); + context = APP_FIXTURE.createAndStart(); } @Test @@ -61,22 +58,13 @@ public void listNodesTest() { Assert.assertTrue(walletStub.listNodes(EmptyMessage.getDefaultInstance()) .getNodesList().isEmpty()); } finally { - // Properly shutdown the gRPC channel to prevent resource leaks - channel.shutdown(); - try { - if (!channel.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { - channel.shutdownNow(); - } - } catch (InterruptedException e) { - channel.shutdownNow(); - Thread.currentThread().interrupt(); - } + ClassLevelAppContextFixture.shutdownChannel(channel); } } @AfterClass public static void destroy() { - context.destroy(); + APP_FIXTURE.close(); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java index 42ed21312c3..5feaf0e5223 100644 --- a/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java @@ -18,21 +18,21 @@ import org.tron.api.GrpcAPI; import org.tron.api.WalletGrpc; import org.tron.api.WalletSolidityGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; import org.tron.core.ChainBaseManager; import org.tron.core.Constant; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @Slf4j public class LiteFnQueryGrpcInterceptorTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelSolidity = null; private static ManagedChannel channelpBFT = null; @@ -84,30 +84,21 @@ public static void init() throws IOException { .usePlaintext() .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); blockingStubSolidity = WalletSolidityGrpc.newBlockingStub(channelSolidity); blockingStubpBFT = WalletSolidityGrpc.newBlockingStub(channelpBFT); chainBaseManager = context.getBean(ChainBaseManager.class); - Application appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } /** * destroy the context. */ @AfterClass - public static void destroy() throws InterruptedException { - if (channelFull != null) { - channelFull.shutdownNow(); - } - if (channelSolidity != null) { - channelSolidity.shutdownNow(); - } - if (channelpBFT != null) { - channelpBFT.shutdownNow(); - } - context.close(); + public static void destroy() { + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelSolidity, channelpBFT); + APP_FIXTURE.close(); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java index 817693dc630..07821d10343 100644 --- a/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java @@ -31,14 +31,12 @@ import org.tron.api.GrpcAPI.TransactionIdList; import org.tron.api.WalletGrpc; import org.tron.api.WalletSolidityGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; import org.tron.core.Constant; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.services.RpcApiService; import org.tron.protos.Protocol.Transaction; @@ -47,6 +45,8 @@ public class RpcApiAccessInterceptorTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelPBFT = null; private static ManagedChannel channelSolidity = null; @@ -93,14 +93,13 @@ public static void init() throws IOException { .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); blockingStubSolidity = WalletSolidityGrpc.newBlockingStub(channelSolidity); blockingStubPBFT = WalletSolidityGrpc.newBlockingStub(channelPBFT); - Application appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } /** @@ -108,16 +107,8 @@ public static void init() throws IOException { */ @AfterClass public static void destroy() { - if (channelFull != null) { - channelFull.shutdownNow(); - } - if (channelPBFT != null) { - channelPBFT.shutdownNow(); - } - if (channelSolidity != null) { - channelSolidity.shutdownNow(); - } - context.close(); + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelPBFT, channelSolidity); + APP_FIXTURE.close(); Args.clearParam(); } @@ -307,4 +298,3 @@ public void testGetMemoFee() { } } - diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java index 72ac126e394..22d8d50483f 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java @@ -3,9 +3,12 @@ import com.google.common.cache.Cache; import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ReflectUtils; import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; @@ -134,15 +137,16 @@ public void testQpsRateLimiterAdapter() { long t0 = System.currentTimeMillis(); CountDownLatch latch = new CountDownLatch(20); - for (int i = 0; i < 20; i++) { - Thread thread = new Thread(new AdaptorThread(latch, strategy)); - thread.start(); - } - + ExecutorService pool = ExecutorServiceManager.newFixedThreadPool("adaptor-test", 20); try { + for (int i = 0; i < 20; i++) { + pool.execute(new AdaptorThread(latch, strategy)); + } latch.await(); } catch (InterruptedException e) { - System.out.println(e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(pool, "adaptor-test"); } long t1 = System.currentTimeMillis(); Assert.assertTrue(t1 - t0 > 4000); diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java index 4ffe732348e..afcf21e7510 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java @@ -19,7 +19,7 @@ public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { - System.out.println(e.getMessage()); + Thread.currentThread().interrupt(); } latch.countDown(); } diff --git a/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java b/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java index ea622213f45..5c365eb3ef0 100644 --- a/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java +++ b/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java @@ -9,15 +9,20 @@ import java.util.Collections; import java.util.HashSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedConstruction; import org.mockito.junit.MockitoJUnitRunner; import org.reflections.Reflections; +import org.tron.common.es.ExecutorServiceManager; import org.tron.core.actuator.AbstractActuator; import org.tron.core.actuator.TransferActuator; import org.tron.core.config.args.Args; @@ -49,25 +54,32 @@ public void testAlreadyRegisteredSkipRegistration() { @Test public void testConcurrentAccessThreadSafe() throws InterruptedException { final int threadCount = 5; - Thread[] threads = new Thread[threadCount]; final AtomicBoolean testPassed = new AtomicBoolean(true); + ExecutorService executor = ExecutorServiceManager + .newFixedThreadPool("transaction-register-test", threadCount); + Future[] futures = new Future[threadCount]; + + try { + for (int i = 0; i < threadCount; i++) { + futures[i] = executor.submit(() -> { + try { + TransactionRegister.registerActuator(); + } catch (Throwable e) { + testPassed.set(false); + throw e; + } + }); + } - for (int i = 0; i < threadCount; i++) { - threads[i] = new Thread(() -> { + for (Future future : futures) { try { - TransactionRegister.registerActuator(); - } catch (Throwable e) { - testPassed.set(false); + future.get(); + } catch (ExecutionException e) { + Assert.fail("Concurrent registration should not throw: " + e.getCause()); } - }); - } - - for (Thread thread : threads) { - thread.start(); - } - - for (Thread thread : threads) { - thread.join(); + } + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(executor, "transaction-register-test"); } assertTrue("All threads should complete without exceptions", testPassed.get()); @@ -134,4 +146,4 @@ public void testThrowsTronError() { assertTrue(error.getMessage().contains("TransferActuator")); } } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java b/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java index 59f4e899d9f..7749cd4ee6a 100644 --- a/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java +++ b/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java @@ -17,6 +17,7 @@ import org.tron.core.config.args.Args; import org.tron.core.consensus.ConsensusService; import org.tron.core.consensus.ProposalController; +import org.tron.core.exception.ItemNotFoundException; import org.tron.core.store.DynamicPropertiesStore; import org.tron.protos.Protocol.Proposal; import org.tron.protos.Protocol.Proposal.State; @@ -29,7 +30,7 @@ public class ProposalControllerTest extends BaseTest { private static boolean init; static { - Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); + Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); } @Before @@ -66,7 +67,7 @@ public void testSetDynamicParameters() { } @Test - public void testProcessProposal() { + public void testProcessProposal() throws ItemNotFoundException { ProposalCapsule proposalCapsule = new ProposalCapsule( Proposal.newBuilder().build()); proposalCapsule.setState(State.PENDING); @@ -77,11 +78,7 @@ public void testProcessProposal() { proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.DISAPPROVED, proposalCapsule.getState()); proposalCapsule.setState(State.PENDING); @@ -92,11 +89,7 @@ public void testProcessProposal() { proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.DISAPPROVED, proposalCapsule.getState()); List activeWitnesses = Lists.newArrayList(); @@ -114,17 +107,13 @@ public void testProcessProposal() { dbManager.getProposalStore().put(key, proposalCapsule); proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.APPROVED, proposalCapsule.getState()); } @Test - public void testProcessProposals() { + public void testProcessProposals() throws ItemNotFoundException { ProposalCapsule proposalCapsule1 = new ProposalCapsule( Proposal.newBuilder().build()); proposalCapsule1.setState(State.APPROVED); @@ -163,11 +152,7 @@ public void testProcessProposals() { proposalController.processProposals(); - try { - proposalCapsule3 = dbManager.getProposalStore().get(proposalCapsule3.createDbKey()); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule3 = dbManager.getProposalStore().get(proposalCapsule3.createDbKey()); Assert.assertEquals(State.DISAPPROVED, proposalCapsule3.getState()); } @@ -181,26 +166,26 @@ public void testHasMostApprovals() { List activeWitnesses = Lists.newArrayList(); for (int i = 0; i < 27; i++) { - activeWitnesses.add(ByteString.copyFrom(new byte[]{(byte) i})); + activeWitnesses.add(ByteString.copyFrom(new byte[] {(byte) i})); } for (int i = 0; i < 18; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } Assert.assertTrue(proposalCapsule.hasMostApprovals(activeWitnesses)); proposalCapsule.clearApproval(); for (int i = 1; i < 18; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } activeWitnesses.clear(); for (int i = 0; i < 5; i++) { - activeWitnesses.add(ByteString.copyFrom(new byte[]{(byte) i})); + activeWitnesses.add(ByteString.copyFrom(new byte[] {(byte) i})); } proposalCapsule.clearApproval(); for (int i = 0; i < 3; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } Assert.assertTrue(proposalCapsule.hasMostApprovals(activeWitnesses)); } diff --git a/framework/src/test/java/org/tron/keystore/CredentialsTest.java b/framework/src/test/java/org/tron/keystore/CredentialsTest.java index df1b4440e08..a072253ff58 100644 --- a/framework/src/test/java/org/tron/keystore/CredentialsTest.java +++ b/framework/src/test/java/org/tron/keystore/CredentialsTest.java @@ -24,9 +24,7 @@ private SignInterface mockSignInterface(byte[] address) { @Test public void testCreate() { SignInterface signInterface = mockSignInterface(ADDRESS_1); - Credentials credentials = Credentials.create(signInterface); - Assert.assertEquals("Credentials address create failed!", StringUtil.encode58Check(ADDRESS_1), credentials.getAddress()); Assert.assertSame("Credentials cryptoEngine create failed", signInterface, @@ -35,14 +33,11 @@ public void testCreate() { @Test public void testCreateFromSM2() { - try { - Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff" - + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - + "fffffffffffffffffffffffffffffffffffffff"))); - Assert.fail("Expected IllegalArgumentException"); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalArgumentException); - } + Exception e = Assert.assertThrows(Exception.class, + () -> Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffff")))); + Assert.assertTrue(e instanceof IllegalArgumentException); } @Test diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java index 7d94f813b80..f5b525cd445 100755 --- a/framework/src/test/java/org/tron/program/SolidityNodeTest.java +++ b/framework/src/test/java/org/tron/program/SolidityNodeTest.java @@ -1,8 +1,16 @@ 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.mockito.Mockito.mock; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -10,9 +18,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; +import org.mockito.InOrder; 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.PublicMethod; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; @@ -89,4 +100,94 @@ public void testSolidityNodeHttpApiService() { solidityNodeHttpApiService.stop(); Assert.assertTrue(true); } + + @Test + public void testAwaitShutdownAlwaysStopsNode() { + Application app = mock(Application.class); + SolidityNode node = mock(SolidityNode.class); + + SolidityNode.awaitShutdown(app, node); + + InOrder inOrder = inOrder(app, node); + inOrder.verify(app).blockUntilShutdown(); + inOrder.verify(node).shutdown(); + } + + @Test + public void testAwaitShutdownStopsNodeWhenBlockedCallFails() { + Application app = mock(Application.class); + SolidityNode node = mock(SolidityNode.class); + RuntimeException expected = new RuntimeException("boom"); + doThrow(expected).when(app).blockUntilShutdown(); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> SolidityNode.awaitShutdown(app, node)); + assertSame(expected, thrown); + + InOrder inOrder = inOrder(app, node); + inOrder.verify(app).blockUntilShutdown(); + inOrder.verify(node).shutdown(); + } + + @Test + public void testShutdownSetsFlagAndShutsDownExecutors() throws Exception { + SolidityNode node = mock(SolidityNode.class); + doCallRealMethod().when(node).shutdown(); + + ExecutorService es1 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-get"); + ExecutorService es2 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-process"); + + Field flagField = SolidityNode.class.getDeclaredField("flag"); + flagField.setAccessible(true); + flagField.set(node, true); + + Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); + getBlockEsField.setAccessible(true); + getBlockEsField.set(node, es1); + + Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); + processBlockEsField.setAccessible(true); + processBlockEsField.set(node, es2); + + node.shutdown(); + + Assert.assertFalse((boolean) flagField.get(node)); + Assert.assertTrue(es1.isShutdown()); + Assert.assertTrue(es2.isShutdown()); + } + + @Test + public void testRunInitializesNamedExecutors() throws Exception { + rpcApiService.start(); + String originalAddr = Args.getInstance().getTrustNodeAddr(); + Args.getInstance().setTrustNodeAddr("127.0.0.1:" + rpcPort); + try { + SolidityNode node = new SolidityNode(dbManager); + + Field flagField = SolidityNode.class.getDeclaredField("flag"); + flagField.setAccessible(true); + flagField.set(node, false); + + Method runMethod = SolidityNode.class.getDeclaredMethod("run"); + runMethod.setAccessible(true); + runMethod.invoke(node); + + Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); + getBlockEsField.setAccessible(true); + Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); + processBlockEsField.setAccessible(true); + + ExecutorService getBlockEs = (ExecutorService) getBlockEsField.get(node); + ExecutorService processBlockEs = (ExecutorService) processBlockEsField.get(node); + + Assert.assertNotNull(getBlockEs); + Assert.assertNotNull(processBlockEs); + + ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "test-solid-get"); + ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "test-solid-process"); + } finally { + Args.getInstance().setTrustNodeAddr(originalAddr); + rpcApiService.stop(); + } + } } diff --git a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java index 60db838a80d..07bddc461a0 100644 --- a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java @@ -90,7 +90,6 @@ public void clear() { public void testTools(String dbType, int checkpointVersion) throws InterruptedException, IOException { logger.info("dbType {}, checkpointVersion {}", dbType, checkpointVersion); - dbPath = String.format("%s_%s_%d", dbPath, dbType, System.currentTimeMillis()); init(dbType); final String[] argsForSnapshot = new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", @@ -166,7 +165,8 @@ private void generateSomeTransactions(int during) { try { Thread.sleep(sleepOnce); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + return; } if ((runTime += sleepOnce) > during) { return; diff --git a/plugins/src/test/java/org/tron/plugins/DbMoveTest.java b/plugins/src/test/java/org/tron/plugins/DbMoveTest.java index 5b25739f272..ec4f0d545b0 100644 --- a/plugins/src/test/java/org/tron/plugins/DbMoveTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbMoveTest.java @@ -31,7 +31,6 @@ private void init(DbTool.DbType dbType, String path) throws IOException, RocksDB DbTool.getDB(path, ACCOUNT, dbType).close(); DbTool.getDB(path, DBUtils.MARKET_PAIR_PRICE_TO_ORDER, dbType).close(); DbTool.getDB(path, TRANS, dbType).close(); - } @After diff --git a/plugins/src/test/java/org/tron/plugins/DbTest.java b/plugins/src/test/java/org/tron/plugins/DbTest.java index bbcc1a0bbf7..d22addfbae8 100644 --- a/plugins/src/test/java/org/tron/plugins/DbTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbTest.java @@ -18,10 +18,10 @@ public class DbTest { - public String INPUT_DIRECTORY; + protected String INPUT_DIRECTORY; private static final String ACCOUNT = "account"; private static final String MARKET = DBUtils.MARKET_PAIR_PRICE_TO_ORDER; - public CommandLine cli = new CommandLine(new Toolkit()); + protected CommandLine cli = new CommandLine(new Toolkit()); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java b/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java index f5880d82e39..ff73eca25cc 100644 --- a/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java +++ b/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java @@ -1,16 +1,7 @@ package org.tron.plugins.leveldb; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Properties; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -82,41 +73,4 @@ public void testEmpty() { Assert.assertEquals(0, ArchiveManifest.run(args)); } - private static void writeProperty(String filename, String key, String value) throws IOException { - File file = new File(filename); - if (!file.exists()) { - file.createNewFile(); - } - - try (FileInputStream fis = new FileInputStream(file); - OutputStream out = new FileOutputStream(file); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, - StandardCharsets.UTF_8))) { - BufferedReader bf = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); - Properties properties = new Properties(); - properties.load(bf); - properties.setProperty(key, value); - properties.store(bw, "Generated by the application. PLEASE DO NOT EDIT! "); - } catch (Exception e) { - logger.warn("{}", e); - } - } - - /** - * delete directory. - */ - private static boolean deleteDir(File dir) { - if (dir.isDirectory()) { - String[] children = dir.list(); - assert children != null; - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - logger.warn("can't delete dir:" + dir); - return false; - } - } - } - return dir.delete(); - } } diff --git a/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java b/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java index 69dca01e4f8..8c20c2b55be 100644 --- a/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java +++ b/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java @@ -1,16 +1,7 @@ package org.tron.plugins.leveldb; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Properties; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -88,41 +79,4 @@ public void testEmpty() { Assert.assertEquals(0, cli.execute(args)); } - private static void writeProperty(String filename, String key, String value) throws IOException { - File file = new File(filename); - if (!file.exists()) { - file.createNewFile(); - } - - try (FileInputStream fis = new FileInputStream(file); - OutputStream out = new FileOutputStream(file); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, - StandardCharsets.UTF_8))) { - BufferedReader bf = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); - Properties properties = new Properties(); - properties.load(bf); - properties.setProperty(key, value); - properties.store(bw, "Generated by the application. PLEASE DO NOT EDIT! "); - } catch (Exception e) { - logger.warn("{}", e); - } - } - - /** - * delete directory. - */ - private static boolean deleteDir(File dir) { - if (dir.isDirectory()) { - String[] children = dir.list(); - assert children != null; - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - logger.warn("can't delete dir:" + dir); - return false; - } - } - } - return dir.delete(); - } } diff --git a/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java index ab1067fefc3..ebc4074ccc0 100644 --- a/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java +++ b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java @@ -7,7 +7,7 @@ public class DbLiteRocksDbV2Test extends DbLiteTest { @Test - public void testToolsWithRocksDB() throws InterruptedException, IOException { + public void testToolsWithRocksDbV2() throws InterruptedException, IOException { testTools("ROCKSDB", 2); } } From 1a97a8119a7c79006fa49f777e4cffdc7198d00c Mon Sep 17 00:00:00 2001 From: Jeremy Zhang <50477615+warku123@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:05:18 +0800 Subject: [PATCH 29/43] feat(metrics): add block transaction count and SR set change monitoring (#6624) --- .../tron/common/prometheus/MetricKeys.java | 5 + .../tron/common/prometheus/MetricLabels.java | 3 + .../common/prometheus/MetricsCounter.java | 1 + .../common/prometheus/MetricsHistogram.java | 20 +- .../org/tron/common/prometheus/SRMetrics.java | 26 +++ .../consensus/dpos/MaintenanceManager.java | 3 + .../main/java/org/tron/core/db/Manager.java | 5 + .../blockchain/BlockChainMetricManager.java | 7 +- .../tron/common/prometheus/SRMetricsTest.java | 206 ++++++++++++++++++ .../prometheus/PrometheusApiServiceTest.java | 40 +++- 10 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 common/src/main/java/org/tron/common/prometheus/SRMetrics.java create mode 100644 framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java diff --git a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java index 87ab6fae0a3..95a38c4b479 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java @@ -14,6 +14,10 @@ public static class Counter { public static final String TXS = "tron:txs"; public static final String MINER = "tron:miner"; public static final String BLOCK_FORK = "tron:block_fork"; + // witness label: bounded cardinality -- SR candidate pool is finite, rotation is + // infrequent (at most once per maintenance interval); kept for at-a-glance SR + // identification in dashboards rather than requiring log cross-referencing. + public static final String SR_SET_CHANGE = "tron:sr_set_change"; public static final String P2P_ERROR = "tron:p2p_error"; public static final String P2P_DISCONNECT = "tron:p2p_disconnect"; public static final String INTERNAL_SERVICE_FAIL = "tron:internal_service_fail"; @@ -62,6 +66,7 @@ public static class Histogram { public static final String MESSAGE_PROCESS_LATENCY = "tron:message_process_latency_seconds"; public static final String BLOCK_FETCH_LATENCY = "tron:block_fetch_latency_seconds"; public static final String BLOCK_RECEIVE_DELAY = "tron:block_receive_delay_seconds"; + public static final String BLOCK_TRANSACTION_COUNT = "tron:block_transaction_count"; private Histogram() { throw new IllegalStateException("Histogram"); diff --git a/common/src/main/java/org/tron/common/prometheus/MetricLabels.java b/common/src/main/java/org/tron/common/prometheus/MetricLabels.java index 2aa3c1e3378..1f0da214085 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricLabels.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricLabels.java @@ -31,6 +31,8 @@ public static class Counter { public static final String TXS_FAIL_SIG = "sig"; public static final String TXS_FAIL_TAPOS = "tapos"; public static final String TXS_FAIL_DUP = "dup"; + public static final String SR_ADD = "add"; + public static final String SR_REMOVE = "remove"; private Counter() { throw new IllegalStateException("Counter"); @@ -66,6 +68,7 @@ private Gauge() { // Histogram public static class Histogram { + public static final String MINER = "miner"; public static final String TRAFFIC_IN = "in"; public static final String TRAFFIC_OUT = "out"; diff --git a/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java b/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java index 6acdf23b3bc..7231baaba8f 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java @@ -14,6 +14,7 @@ class MetricsCounter { init(MetricKeys.Counter.TXS, "tron txs info .", "type", "detail"); init(MetricKeys.Counter.MINER, "tron miner info .", "miner", "type"); init(MetricKeys.Counter.BLOCK_FORK, "tron block fork info .", "type"); + init(MetricKeys.Counter.SR_SET_CHANGE, "tron sr set change .", "action", "witness"); init(MetricKeys.Counter.P2P_ERROR, "tron p2p error info .", "type"); init(MetricKeys.Counter.P2P_DISCONNECT, "tron p2p disconnect .", "type"); init(MetricKeys.Counter.INTERNAL_SERVICE_FAIL, "internal Service fail.", diff --git a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java index 556db10feb5..6a66dc76bb3 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java @@ -20,7 +20,7 @@ public class MetricsHistogram { init(MetricKeys.Histogram.JSONRPC_SERVICE_LATENCY, "JsonRpc Service latency.", "method"); init(MetricKeys.Histogram.MINER_LATENCY, "miner latency.", - "miner"); + MetricLabels.Histogram.MINER); init(MetricKeys.Histogram.PING_PONG_LATENCY, "node ping pong latency."); init(MetricKeys.Histogram.VERIFY_SIGN_LATENCY, "verify sign latency for trx , block.", "type"); @@ -36,7 +36,7 @@ public class MetricsHistogram { init(MetricKeys.Histogram.PROCESS_TRANSACTION_LATENCY, "process transaction latency.", "type", "contract"); init(MetricKeys.Histogram.MINER_DELAY, "miner delay time, actualTime - planTime.", - "miner"); + MetricLabels.Histogram.MINER); init(MetricKeys.Histogram.UDP_BYTES, "udp_bytes traffic.", "type"); init(MetricKeys.Histogram.TCP_BYTES, "tcp_bytes traffic.", @@ -48,6 +48,11 @@ public class MetricsHistogram { init(MetricKeys.Histogram.BLOCK_FETCH_LATENCY, "fetch block latency."); init(MetricKeys.Histogram.BLOCK_RECEIVE_DELAY, "receive block delay time, receiveTime - blockTime."); + + init(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, + "Distribution of transaction counts per block.", + new double[]{0, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000}, + MetricLabels.Histogram.MINER); } private MetricsHistogram() { @@ -62,6 +67,17 @@ private static void init(String name, String help, String... labels) { .register()); } + private static void init(String name, String help, double[] buckets, String... labels) { + Histogram.Builder builder = Histogram.build() + .name(name) + .help(help) + .labelNames(labels); + if (buckets != null && buckets.length > 0) { + builder.buckets(buckets); + } + container.put(name, builder.register()); + } + static Histogram.Timer startTimer(String key, String... labels) { if (Metrics.enabled()) { Histogram histogram = container.get(key); diff --git a/common/src/main/java/org/tron/common/prometheus/SRMetrics.java b/common/src/main/java/org/tron/common/prometheus/SRMetrics.java new file mode 100644 index 00000000000..0c547a38e2c --- /dev/null +++ b/common/src/main/java/org/tron/common/prometheus/SRMetrics.java @@ -0,0 +1,26 @@ +package org.tron.common.prometheus; + +import com.google.protobuf.ByteString; +import java.util.List; +import org.tron.common.utils.StringUtil; + +public class SRMetrics { + + private SRMetrics() { + throw new IllegalStateException("SRMetrics"); + } + + public static void recordSrSetChange(List currentWits, List newWits) { + if (!Metrics.enabled()) { + return; + } + newWits.stream() + .filter(w -> !currentWits.contains(w)) + .forEach(w -> Metrics.counterInc(MetricKeys.Counter.SR_SET_CHANGE, 1, + MetricLabels.Counter.SR_ADD, StringUtil.encode58Check(w.toByteArray()))); + currentWits.stream() + .filter(w -> !newWits.contains(w)) + .forEach(w -> Metrics.counterInc(MetricKeys.Counter.SR_SET_CHANGE, 1, + MetricLabels.Counter.SR_REMOVE, StringUtil.encode58Check(w.toByteArray()))); + } +} diff --git a/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java b/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java index 012169bdb87..fd5e4364d0d 100644 --- a/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java +++ b/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java @@ -16,6 +16,7 @@ import org.bouncycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.prometheus.SRMetrics; import org.tron.consensus.ConsensusDelegate; import org.tron.consensus.pbft.PbftManager; import org.tron.core.capsule.AccountCapsule; @@ -141,6 +142,8 @@ public void doMaintenance() { witnessCapsule.setIsJobs(true); consensusDelegate.saveWitness(witnessCapsule); }); + + SRMetrics.recordSrSetChange(currentWits, newWits); } logger.info("Update witness success. \nbefore: {} \nafter: {}", 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 cd1a61c01fe..6ccd024091d 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1270,6 +1270,11 @@ public void pushBlock(final BlockCapsule block) synchronized (this) { Metrics.histogramObserve(blockedTimer.get()); blockedTimer.remove(); + if (Metrics.enabled()) { + Metrics.histogramObserve(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, + block.getTransactions().size(), + StringUtil.encode58Check(block.getWitnessAddress().toByteArray())); + } long headerNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); if (block.getNum() <= headerNumber && khaosDb.containBlockInMiniStore(block.getBlockId())) { logger.info("Block {} is already exist.", block.getBlockId().getString()); diff --git a/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java b/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java index 384f1d8add1..f39cf66a8ad 100644 --- a/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java +++ b/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java @@ -164,9 +164,10 @@ public void applyBlock(BlockCapsule block) { } //TPS - if (block.getTransactions().size() > 0) { - MetricsUtil.meterMark(MetricsKey.BLOCKCHAIN_TPS, block.getTransactions().size()); - Metrics.counterInc(MetricKeys.Counter.TXS, block.getTransactions().size(), + int txCount = block.getTransactions().size(); + if (txCount > 0) { + MetricsUtil.meterMark(MetricsKey.BLOCKCHAIN_TPS, txCount); + Metrics.counterInc(MetricKeys.Counter.TXS, txCount, MetricLabels.Counter.TXS_SUCCESS, MetricLabels.Counter.TXS_SUCCESS); } } diff --git a/framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java b/framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java new file mode 100644 index 00000000000..4c2e9292d29 --- /dev/null +++ b/framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java @@ -0,0 +1,206 @@ +package org.tron.common.prometheus; + +import com.google.protobuf.ByteString; +import io.prometheus.client.CollectorRegistry; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.StringUtil; +import org.tron.consensus.dpos.MaintenanceManager; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.VotesCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.consensus.ConsensusService; +import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Vote; + +@Slf4j(topic = "metric") +public class SRMetricsTest extends BaseTest { + + private static final AtomicInteger PORT = new AtomicInteger(0); + private static final AtomicInteger UNIQUE = new AtomicInteger(0); + + @Resource + private MaintenanceManager maintenanceManager; + @Resource + private ConsensusService consensusService; + + static { + Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); + Args.getInstance().setNodeListenPort(20000 + PORT.incrementAndGet()); + Args.getInstance().setMetricsPrometheusEnable(true); + Metrics.init(); + } + + @Before + public void setUp() { + Args.getInstance().setMetricsPrometheusEnable(true); + consensusService.start(); + } + + @After + public void tearDown() { + Args.getInstance().setMetricsPrometheusEnable(true); + } + + /** + * Drive the full maintenance flow: starting with a single active witness while WitnessStore + * contains additional ones, doMaintenance() should expand active witnesses to the full set and + * emit SR_ADD for each newly active witness. + */ + @Test + public void testSrAddViaMaintenance() { + ByteString stableWit = registerWitness(); + ByteString newWit1 = registerWitness(); + ByteString newWit2 = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + seedVote(stableWit); + + maintenanceManager.doMaintenance(); + + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_ADD, newWit1).intValue()); + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_ADD, newWit2).intValue()); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, stableWit)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, stableWit)); + } + + /** + * Active witness set already matches WitnessStore → no metric emitted. + */ + @Test + public void testNoMetricWhenSetUnchanged() { + ByteString witA = registerWitness(); + ByteString witB = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Arrays.asList(witA, witB)); + + seedVote(witA); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, witA)); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, witB)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, witA)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, witB)); + } + + /** + * Empty VotesStore → countVote() is empty → SR change check is skipped, even when the active + * set differs from the full witness store. + */ + @Test + public void testNoMetricWhenNoVotes() { + ByteString stableWit = registerWitness(); + ByteString newWit = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, newWit)); + } + + /** + * Metrics disabled → record() short-circuits even though the active set changes. + */ + @Test + public void testNoMetricWhenMetricsDisabled() { + Args.getInstance().setMetricsPrometheusEnable(false); + try { + ByteString stableWit = registerWitness(); + ByteString newWit = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + seedVote(stableWit); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, newWit)); + } finally { + Args.getInstance().setMetricsPrometheusEnable(true); + } + } + + /** + * SR_REMOVE is verified by directly calling record() instead of going through doMaintenance(), + * because driving a removal through the real flow is impractical here: + * + *

Inside doMaintenance(), the block before SRMetrics.recordSrSetChange() iterates currentWits + * and calls setIsJobs(false) on each WitnessCapsule fetched from WitnessStore. If currentWits + * contains any address that is not present in WitnessStore, getWitness() returns null and the + * code NPEs — so SR_REMOVE cannot be triggered by simply pointing the active set at an + * "obsolete" address. + * + *

The only other path to SR_REMOVE is rank-based eviction: with more than + * MAX_ACTIVE_WITNESS_NUM (27) witnesses, sorting drops the lowest-ranked one. Building that + * setup just to exercise this branch is heavy and adds little value, since SR_ADD and + * SR_REMOVE share the exact same emit logic in record() — verifying SR_ADD via doMaintenance + * already proves the wiring is correct, and this direct call covers the symmetric branch. + */ + @Test + public void testSrRemoveDirect() { + ByteString stableWit = uniqueAddress(); + ByteString removedWit = uniqueAddress(); + + SRMetrics.recordSrSetChange( + Arrays.asList(stableWit, removedWit), + Collections.singletonList(stableWit)); + + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_REMOVE, removedWit).intValue()); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, removedWit)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, stableWit)); + } + + private ByteString registerWitness() { + ByteString address = uniqueAddress(); + chainBaseManager.getWitnessStore().put(address.toByteArray(), new WitnessCapsule(address)); + chainBaseManager.addWitness(address); + chainBaseManager.getAccountStore().put(address.toByteArray(), + new AccountCapsule(Protocol.Account.newBuilder().setAddress(address).build())); + return address; + } + + private void seedVote(ByteString voteFor) { + ByteString voter = uniqueAddress(); + VotesCapsule votes = new VotesCapsule(voter, Collections.emptyList(), + Collections.singletonList(Vote.newBuilder() + .setVoteAddress(voteFor) + .setVoteCount(1L) + .build())); + chainBaseManager.getVotesStore().put(voter.toByteArray(), votes); + } + + private ByteString uniqueAddress() { + int n = UNIQUE.incrementAndGet(); + byte[] bytes = new byte[21]; + bytes[0] = 0x41; + bytes[17] = (byte) ((n >> 16) & 0xFF); + bytes[18] = (byte) ((n >> 8) & 0xFF); + bytes[19] = (byte) (n & 0xFF); + bytes[20] = 0x01; + return ByteString.copyFrom(bytes); + } + + private Double sample(String action, ByteString witness) { + return CollectorRegistry.defaultRegistry.getSampleValue( + MetricKeys.Counter.SR_SET_CHANGE + "_total", + new String[]{"action", "witness"}, + new String[]{action, StringUtil.encode58Check(witness.toByteArray())}); + } +} diff --git a/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java b/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java index d4d758b7a98..dd260a1b869 100644 --- a/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java +++ b/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java @@ -7,6 +7,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -25,6 +26,7 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.StringUtil; import org.tron.common.utils.Utils; import org.tron.consensus.dpos.DposSlot; import org.tron.core.ChainBaseManager; @@ -38,6 +40,8 @@ @Slf4j(topic = "metric") public class PrometheusApiServiceTest extends BaseTest { + + static LocalDateTime localDateTime = LocalDateTime.now(); @Resource private DposSlot dposSlot; @@ -65,7 +69,7 @@ protected static void initParameter(CommonParameter parameter) { parameter.setMetricsPrometheusEnable(true); } - protected void check() throws Exception { + protected void check(byte[] address, Map witnessAndAccount) throws Exception { Double memoryBytes = CollectorRegistry.defaultRegistry.getSampleValue( "system_total_physical_memory_bytes"); Assert.assertNotNull(memoryBytes); @@ -80,6 +84,32 @@ protected void check() throws Exception { new String[] {"sync"}, new String[] {"false"}); Assert.assertNotNull(pushBlock); Assert.assertEquals(pushBlock.intValue(), blocks + 1); + + String minerBase58 = StringUtil.encode58Check(address); + // Query histogram bucket le="0.0" for empty blocks + Double emptyBlock = CollectorRegistry.defaultRegistry.getSampleValue( + "tron:block_transaction_count_bucket", + new String[] {MetricLabels.Histogram.MINER, "le"}, new String[] {minerBase58, "0.0"}); + + Assert.assertNotNull("Empty block bucket should exist for miner: " + minerBase58, emptyBlock); + Assert.assertEquals("Should have 1 empty block", 1, emptyBlock.intValue()); + + // Collect empty blocks for each new witness in witnessAndAccount (excluding initial address) + ByteString addressByteString = ByteString.copyFrom(address); + int totalNewWitnessEmptyBlocks = 0; + for (ByteString witnessAddress : witnessAndAccount.keySet()) { + if (witnessAddress.equals(addressByteString)) { + continue; + } + String witnessBase58 = StringUtil.encode58Check(witnessAddress.toByteArray()); + int witnessEmptyBlock = CollectorRegistry.defaultRegistry.getSampleValue( + "tron:block_transaction_count_bucket", + new String[] {MetricLabels.Histogram.MINER, "le"}, new String[] {witnessBase58, "0.0"}) + .intValue(); + totalNewWitnessEmptyBlocks += witnessEmptyBlock; + } + Assert.assertEquals(blocks, totalNewWitnessEmptyBlocks); + Double errorLogs = CollectorRegistry.defaultRegistry.getSampleValue( "tron:error_info_total", new String[] {"net"}, new String[] {MetricLabels.UNDEFINED}); Assert.assertNull(errorLogs); @@ -130,10 +160,16 @@ public void testMetric() throws Exception { Map witnessAndAccount = addTestWitnessAndAccount(); witnessAndAccount.put(ByteString.copyFrom(address), key); + + // Schedule the new witnesses (excluding initial address) so dposSlot rotates blocks among them + List newActiveWitnesses = new ArrayList<>(witnessAndAccount.keySet()); + newActiveWitnesses.remove(ByteString.copyFrom(address)); + chainBaseManager.getWitnessScheduleStore().saveActiveWitnesses(newActiveWitnesses); + for (int i = 0; i < blocks; i++) { generateBlock(witnessAndAccount); } - check(); + check(address, witnessAndAccount); } private Map addTestWitnessAndAccount() { From 314f130f4b4b9b9e6b3495f4aff1ffb2a696836c Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 30 Apr 2026 11:46:10 +0800 Subject: [PATCH 30/43] refactor(jsonrpc): centralize block selector parsing into JsonRpcApiUtil (#6668) --- .../src/main/java/org/tron/core/Wallet.java | 41 +- .../core/services/jsonrpc/JsonRpcApiUtil.java | 65 ++- .../services/jsonrpc/TronJsonRpcImpl.java | 186 ++++---- .../jsonrpc/filters/LogFilterWrapper.java | 64 +-- .../tron/core/jsonrpc/JsonrpcServiceTest.java | 402 +++++++++++++++--- .../jsonrpc/LogFilterWrapperStrategyTest.java | 162 +++++++ 6 files changed, 691 insertions(+), 229 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..efbceda1b16 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -32,11 +32,6 @@ import static org.tron.core.config.Parameter.DatabaseConstants.PROPOSAL_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.WITNESS_COUNT_LIMIT_MAX; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseEnergyFee; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; import static org.tron.core.vm.utils.FreezeV2Util.getV2EnergyUsage; import static org.tron.core.vm.utils.FreezeV2Util.getV2NetUsage; import static org.tron.protos.contract.Common.ResourceCode; @@ -193,7 +188,6 @@ import org.tron.core.exception.VMIllegalException; import org.tron.core.exception.ValidateSignatureException; import org.tron.core.exception.ZksnarkException; -import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.TronNetService; import org.tron.core.net.message.adv.TransactionMessage; @@ -711,6 +705,10 @@ public long getSolidBlockNum() { return chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); } + public long getHeadBlockNum() { + return chainBaseManager.getHeadBlockNum(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -733,37 +731,6 @@ public long getTransactionCountByBlockNum(long blockNum) { return count; } - public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(id)) { - return getBlockByNum(0); - } else if (LATEST_STR.equalsIgnoreCase(id)) { - return getNowBlock(); - } else if (FINALIZED_STR.equalsIgnoreCase(id)) { - return getSolidBlock(); - } else if (PENDING_STR.equalsIgnoreCase(id)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); - } else { - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(id).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid block number"); - } - - return getBlockByNum(blockNumber); - } - } - - public List getTransactionsByJsonBlockId(String id) - throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(id)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); - } else { - Block block = getByJsonBlockId(id); - return block != null ? block.getTransactionsList() : null; - } - } - public WitnessList getWitnessList() { WitnessList.Builder builder = WitnessList.newBuilder(); List witnessCapsuleList = chainBaseManager.getWitnessStore().getAllWitnesses(); 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 4a60f14b534..08c1068e3a2 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 @@ -1,11 +1,5 @@ package org.tron.core.services.jsonrpc; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; - import com.google.common.base.Throwables; import com.google.common.primitives.Longs; import com.google.protobuf.Any; @@ -57,6 +51,15 @@ @Slf4j(topic = "API") public class JsonRpcApiUtil { + public static final String EARLIEST_STR = "earliest"; + public static final String PENDING_STR = "pending"; + public static final String LATEST_STR = "latest"; + public static final String FINALIZED_STR = "finalized"; + public static final String SAFE_STR = "safe"; + public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; + public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; + public static final String BLOCK_NUM_ERROR = "invalid block number"; + public static byte[] convertToTronAddress(byte[] address) { byte[] newAddress = new byte[21]; byte[] temp = new byte[] {Wallet.getAddressPreFixByte()}; @@ -515,20 +518,52 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { return -1; } - public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) + public static boolean isBlockTag(String tag) { + return LATEST_STR.equalsIgnoreCase(tag) + || EARLIEST_STR.equalsIgnoreCase(tag) + || FINALIZED_STR.equalsIgnoreCase(tag) + || PENDING_STR.equalsIgnoreCase(tag) + || SAFE_STR.equalsIgnoreCase(tag); + } + + /** + * Parse a block tag (latest, earliest, finalized) to block number. + * + *

Note: for "latest", the returned block number may not yet be available in + * blockStore or blockIndexStore due to write ordering. Callers that need the + * actual block must handle the not-found case.

+ */ + public static long parseBlockTag(String tag, Wallet wallet) throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + if (LATEST_STR.equalsIgnoreCase(tag)) { + return wallet.getHeadBlockNum(); } - if (StringUtils.isEmpty(blockNumOrTag) || LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - return -1; - } else if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag)) { + if (EARLIEST_STR.equalsIgnoreCase(tag)) { return 0; - } else if (FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { + } + if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); - } else { - return ByteArray.jsonHexToLong(blockNumOrTag); } + if (PENDING_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + } + if (SAFE_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_SAFE_SUPPORT_ERROR); + } + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + + /** + * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. + * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use + * isBlockTag/parseBlockTag and handle hex separately with hexToBigInteger. + */ + public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) + throws JsonRpcInvalidParamsException { + if (isBlockTag(blockNumOrTag)) { + return parseBlockTag(blockNumOrTag, wallet); + } + return ByteArray.jsonHexToLong(blockNumOrTag); } public static String generateFilterId() { 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 de939bdfff4..72fc579aa56 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 @@ -3,6 +3,9 @@ import static org.tron.core.Wallet.CONTRACT_VALIDATE_ERROR; import static org.tron.core.services.http.Util.setTransactionExtraData; import static org.tron.core.services.http.Util.setTransactionPermissionId; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.BLOCK_NUM_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.FINALIZED_STR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.generateFilterId; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; @@ -152,17 +155,11 @@ public enum RequestSource { public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; - public static final String EARLIEST_STR = "earliest"; - public static final String PENDING_STR = "pending"; - public static final String LATEST_STR = "latest"; - public static final String FINALIZED_STR = "finalized"; - public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; public static final String INVALID_BLOCK_RANGE = "invalid block range params"; private static final String JSON_ERROR = "invalid json request"; - private static final String BLOCK_NUM_ERROR = "invalid block number"; private static final String TAG_NOT_SUPPORT_ERROR = - "TAG [earliest | pending | finalized] not supported"; + "TAG [earliest | pending | finalized | safe] not supported"; private static final String QUANTITY_NOT_SUPPORT_ERROR = "QUANTITY not supported, just support TAG as latest"; private static final String NO_BLOCK_HEADER = "header not found"; @@ -308,12 +305,12 @@ public String ethGetBlockTransactionCountByHash(String blockHash) @Override public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) throws JsonRpcInvalidParamsException { - List list = wallet.getTransactionsByJsonBlockId(blockNumOrTag); - if (list == null) { + Block block = getBlockByNumOrTag(blockNumOrTag); + if (block == null) { return null; } - long n = list.size(); + long n = block.getTransactionsCount(); return ByteArray.toJsonHex(n); } @@ -327,7 +324,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) throws JsonRpcInvalidParamsException { - final Block b = wallet.getByJsonBlockId(blockNumOrTag); + final Block b = getBlockByNumOrTag(blockNumOrTag); return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } @@ -345,11 +342,49 @@ private byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException return bHash; } + /** + * Reject any block selector that is not "latest". + * Accepts "latest" silently; throws for other tags, numeric blocks, or invalid input. + */ + private void requireLatestBlockTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + return; + } + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); + } + try { + ByteArray.hexToBigInteger(blockNumOrTag); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + } + private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsException { byte[] bHash = hashToByteArray(blockHash); return wallet.getBlockById(ByteString.copyFrom(bHash)); } + private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + long blockNum; + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + // Return the head block directly from blockStore, bypassing blockIndexStore + // which may not yet be written when latestBlockHeaderNumber is already updated. + return wallet.getNowBlock(); + } + return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); + } + try { + blockNum = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + return wallet.getBlockByNum(blockNum); + } + private BlockResult getBlockResult(Block block, boolean fullTx) { if (block == null) { return null; @@ -393,30 +428,18 @@ public String getLatestBlockNum() { @Override public String getTrxBalance(String address, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(address); + requireLatestBlockTag(blockNumOrTag); - Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); - Account reply = wallet.getAccount(account); - long balance = 0; + byte[] addressData = addressCompatibleToByteArray(address); - if (reply != null) { - balance = reply.getBalance(); - } - return ByteArray.toJsonHex(balance); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); + Account reply = wallet.getAccount(account); + long balance = 0; - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + if (reply != null) { + balance = reply.getBalance(); } + return ByteArray.toJsonHex(balance); } private void callTriggerConstantContract(byte[] ownerAddressByte, byte[] contractAddressByte, @@ -535,67 +558,42 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va @Override public String getStorageAt(String address, String storageIdx, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressByte = addressCompatibleToByteArray(address); - - // get contract from contractStore - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); - SmartContract smartContract = wallet.getContract(bytesMessage); - if (smartContract == null) { - return ByteArray.toJsonHex(new byte[32]); - } + requireLatestBlockTag(blockNumOrTag); - StorageRowStore store = manager.getStorageRowStore(); - Storage storage = new Storage(addressByte, store); - storage.setContractVersion(smartContract.getVersion()); - storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + byte[] addressByte = addressCompatibleToByteArray(address); - DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); - return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + // get contract from contractStore + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); + SmartContract smartContract = wallet.getContract(bytesMessage); + if (smartContract == null) { + return ByteArray.toJsonHex(new byte[32]); } + + StorageRowStore store = manager.getStorageRowStore(); + Storage storage = new Storage(addressByte, store); + storage.setContractVersion(smartContract.getVersion()); + storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + + DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); + return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } @Override public String getABIOfSmartContract(String contractAddress, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(contractAddress); + requireLatestBlockTag(blockNumOrTag); - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); - SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + byte[] addressData = addressCompatibleToByteArray(contractAddress); - if (contractDataWrapper != null) { - return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); - } else { - return "0x"; - } + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); + SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + if (contractDataWrapper != null) { + return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + return "0x"; } } @@ -803,7 +801,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) throws JsonRpcInvalidParamsException { - Block block = wallet.getByJsonBlockId(blockNumOrTag); + Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } @@ -894,7 +892,7 @@ public List getBlockReceipts(String blockNumOrHashOrTag) if (Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { block = getBlockByJsonHash(blockNumOrHashOrTag); } else { - block = wallet.getByJsonBlockId(blockNumOrHashOrTag); + block = getBlockByNumOrTag(blockNumOrHashOrTag); } // block receipts not available: block is genesis, not produced yet, or pruned in light node @@ -973,7 +971,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) long blockNumber; try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); + blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); } catch (Exception e) { throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } @@ -1003,25 +1001,13 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidRequestException(JSON_ERROR); } - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); - byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); + requireLatestBlockTag(blockNumOrTag); - return call(addressData, contractAddressData, transactionCall.parseValue(), - ByteArray.fromHexString(transactionCall.getData())); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); + byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); - } + return call(addressData, contractAddressData, transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.getData())); } @Override diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 97a012b7f9a..0331ab3694a 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -1,6 +1,7 @@ package org.tron.core.services.jsonrpc.filters; import static org.tron.common.math.StrictMathWrapper.min; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import com.google.protobuf.ByteString; import lombok.Getter; @@ -50,39 +51,50 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet toBlockSrc = fromBlockSrc; } else { - // if fromBlock is empty but toBlock is not empty, - // then if toBlock < maxBlockNum, set fromBlock = toBlock - // then if toBlock >= maxBlockNum, set fromBlock = maxBlockNum - if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isNotEmpty(fr.getToBlock())) { - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (toBlockSrc == -1) { - toBlockSrc = Long.MAX_VALUE; - } - fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + // Normalize the request into one of four strategies based on parameter emptiness. + // Long.MAX_VALUE is an internal sentinel meaning "open upper bound"; it is never + // treated as a real block number by later query stages. + // Note: "latest" tag handling differs by strategy: + // - Strategy 2: toBlock="latest" -> Long.MAX_VALUE (track future blocks) + // - Strategy 3: fromBlock="latest" -> currentMaxBlockNum snapshot (bounded start) + // - Strategy 4: fromBlock="latest" -> currentMaxBlockNum; toBlock="latest" -> Long.MAX_VALUE - } else if (StringUtils.isNotEmpty(fr.getFromBlock()) - && StringUtils.isEmpty(fr.getToBlock())) { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - if (fromBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - } - toBlockSrc = Long.MAX_VALUE; + boolean fromEmpty = StringUtils.isEmpty(fr.getFromBlock()); + boolean toEmpty = StringUtils.isEmpty(fr.getToBlock()); - } else if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isEmpty(fr.getToBlock())) { + if (fromEmpty && toEmpty) { + // Strategy 1: Both parameters omitted. Start at the current head and track new blocks. fromBlockSrc = currentMaxBlockNum; toBlockSrc = Long.MAX_VALUE; - } else { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (fromBlockSrc == -1 && toBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - toBlockSrc = Long.MAX_VALUE; - } else if (fromBlockSrc == -1 && toBlockSrc >= 0) { - fromBlockSrc = currentMaxBlockNum; - } else if (fromBlockSrc >= 0 && toBlockSrc == -1) { + } else if (fromEmpty) { + // Strategy 2: Only toBlock specified. + // If toBlock is "latest": track future blocks (fromBlock = currentMaxBlockNum, + // toBlock = MAX_VALUE). If concrete: bounded query with fromBlock = min(toBlock, + // currentMaxBlockNum). + if (LATEST_STR.equalsIgnoreCase(fr.getToBlock())) { toBlockSrc = Long.MAX_VALUE; + } else { + toBlockSrc = JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); } + fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + + } else if (toEmpty) { + // Strategy 3: Only fromBlock specified. Start at fromBlock and track new blocks. + // If fromBlock is "latest", use the snapshot (currentMaxBlockNum) as the starting point. + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = Long.MAX_VALUE; + + } else { + // Strategy 4: Both parameters specified. + // If fromBlock is "latest": use the snapshot (currentMaxBlockNum) as a fixed start point. + // If toBlock is "latest": use Long.MAX_VALUE to track future blocks. + // Otherwise: parse both as concrete block numbers + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getToBlock()) ? Long.MAX_VALUE + : JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); if (fromBlockSrc > toBlockSrc) { throw new JsonRpcInvalidParamsException("please verify: fromBlock <= toBlock"); } 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 e1a698216c4..362cfa0a126 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1,7 +1,10 @@ package org.tron.core.jsonrpc; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getByJsonBlockId; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_SAFE_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.isBlockTag; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockTag; import com.alibaba.fastjson.JSON; import com.google.gson.JsonArray; @@ -10,6 +13,7 @@ import io.prometheus.client.CollectorRegistry; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -45,6 +49,7 @@ import org.tron.core.services.interfaceJsonRpcOnSolidity.JsonRpcServiceOnSolidity; import org.tron.core.services.jsonrpc.FullNodeJsonRpcHttpService; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; @@ -62,6 +67,8 @@ public class JsonrpcServiceTest extends BaseTest { private static final String OWNER_ADDRESS_ACCOUNT_NAME = "first"; private static final long LATEST_BLOCK_NUM = 10_000L; private static final long LATEST_SOLIDIFIED_BLOCK_NUM = 4L; + private static final String TAG_NOT_SUPPORT_ERROR = + "TAG [earliest | pending | finalized | safe] not supported"; private static TronJsonRpcImpl tronJsonRpc; @Resource @@ -255,17 +262,13 @@ public void testGetBlockTransactionCountByNumber() { Assert.assertNull(result); } - try { - result = tronJsonRpc.ethGetBlockTransactionCountByNumber("pending"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("pending")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); - try { - result = tronJsonRpc.ethGetBlockTransactionCountByNumber("qqqqq"); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception malformedEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("qqqqq")); + Assert.assertEquals("invalid block number", malformedEx.getMessage()); try { result = tronJsonRpc.ethGetBlockTransactionCountByNumber("latest"); @@ -282,6 +285,15 @@ public void testGetBlockTransactionCountByNumber() { } Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTransactions().size()), result); + // safe tag is not supported (new tag added in this refactor) + Exception safeEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("safe")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + + // hex that overflows long -> longValueExact rejects (previously silently truncated) + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("0x10000000000000000")); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); } @Test @@ -353,6 +365,16 @@ public void testGetBlockByNumber() { Exception e2 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.ethGetBlockByNumber("0x", false)); Assert.assertEquals("invalid block number", e2.getMessage()); + + // safe + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("safe", false)); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e3.getMessage()); + + // hex overflows long -> longValueExact rejects + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("0x10000000000000000", false)); + Assert.assertEquals("invalid block number", e4.getMessage()); } @Test @@ -423,47 +445,67 @@ public void testServicesInit() { } @Test - public void testGetByJsonBlockId() { - long blkNum = 0; - + public void testBlockTagParsing() { + // isBlockTag + Assert.assertTrue(isBlockTag("pending")); + Assert.assertTrue(isBlockTag("latest")); + Assert.assertTrue(isBlockTag("earliest")); + Assert.assertTrue(isBlockTag("finalized")); + Assert.assertTrue(isBlockTag("safe")); + Assert.assertFalse(isBlockTag(null)); + Assert.assertFalse(isBlockTag("0xa")); + Assert.assertFalse(isBlockTag("")); + + // parseBlockTag: pending throws Exception pendingEx = Assert.assertThrows(Exception.class, - () -> getByJsonBlockId("pending", wallet)); - Assert.assertEquals("TAG pending not supported", pendingEx.getMessage()); + () -> parseBlockTag("pending", wallet)); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); + + // parseBlockTag: safe throws + Exception safeEx = Assert.assertThrows(Exception.class, + () -> parseBlockTag("safe", wallet)); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + // parseBlockTag: latest -> headBlockNum try { - blkNum = getByJsonBlockId(null, wallet); + long blkNum = parseBlockTag("latest", wallet); + Assert.assertEquals(LATEST_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: earliest -> 0 try { - blkNum = getByJsonBlockId("latest", wallet); + long blkNum = parseBlockTag("earliest", wallet); + Assert.assertEquals(0L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: finalized -> solidBlockNum try { - blkNum = getByJsonBlockId("finalized", wallet); + long blkNum = parseBlockTag("finalized", wallet); + Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); + // parseBlockNumber: hex -> number try { - blkNum = getByJsonBlockId("0xa", wallet); + long blkNum = parseBlockNumber("0xa", wallet); + Assert.assertEquals(10L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(10L, blkNum); + // parseBlockNumber: bad hex -> throws Exception abcEx = Assert.assertThrows(Exception.class, - () -> getByJsonBlockId("abc", wallet)); + () -> parseBlockNumber("abc", wallet)); Assert.assertEquals("Incorrect hex syntax", abcEx.getMessage()); + // parseBlockNumber: malformed hex -> throws Exception hexEx = Assert.assertThrows(Exception.class, - () -> getByJsonBlockId("0xxabc", wallet)); + () -> parseBlockNumber("0xxabc", wallet)); // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); } @@ -474,18 +516,19 @@ public void testGetTrxBalance() { Exception e1 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getTrxBalance("", "earliest")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e1.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); Exception e2 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getTrxBalance("", "pending")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e2.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); Exception e3 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getTrxBalance("", "finalized")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e3.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); try { balance = tronJsonRpc.getTrxBalance("0xabd4b9367799eaa3197fecb144eb71de1e049abc", @@ -500,54 +543,219 @@ public void testGetTrxBalance() { public void testGetStorageAt() { Exception e1 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getStorageAt("", "", "earliest")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e1.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); Exception e2 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getStorageAt("", "", "pending")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e2.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); Exception e3 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getStorageAt("", "", "finalized")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e3.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "0x1")); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e5.getMessage()); + + // malformed hex -> BLOCK_NUM_ERROR + Exception e6 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "abc")); + Assert.assertEquals("invalid block number", e6.getMessage()); + + // latest happy path: address is an account, not a contract, so returns 32 zero bytes + try { + String value = tronJsonRpc.getStorageAt( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "0x0", "latest"); + Assert.assertEquals(ByteArray.toJsonHex(new byte[32]), value); + } catch (Exception e) { + Assert.fail(); + } } @Test public void testGetABIOfSmartContract() { Exception e1 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getABIOfSmartContract("", "earliest")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e1.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); Exception e2 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getABIOfSmartContract("", "pending")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e2.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); Exception e3 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getABIOfSmartContract("", "finalized")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e3.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "0x1")); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e5.getMessage()); + + // malformed hex -> BLOCK_NUM_ERROR + Exception e6 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "abc")); + Assert.assertEquals("invalid block number", e6.getMessage()); + + // latest happy path: address is an account, not a contract, so returns "0x" + try { + String code = tronJsonRpc.getABIOfSmartContract( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "latest"); + Assert.assertEquals("0x", code); + } catch (Exception e) { + Assert.fail(); + } } @Test public void testGetCall() { Exception e1 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getCall(null, "earliest")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e1.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); Exception e2 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getCall(null, "pending")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e2.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); Exception e3 = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getCall(null, "finalized")); - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e3.getMessage()); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + } + + @Test + public void testGetTransactionByBlockNumberAndIndex() { + // valid hex block number: blockCapsule1 has 2 txs; index 0 is transactionCapsule1. + // Assert the returned tx actually resolves to transactionCapsule1's hash, + // block number, and index rather than just non-null. + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals( + ByteArray.toJsonHex(transactionCapsule1.getTransactionId().getBytes()), + result.getHash()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); + Assert.assertEquals(ByteArray.toJsonHex(0L), result.getTransactionIndex()); + } catch (Exception e) { + Assert.fail(); + } + + // index out of range in an existing block returns null + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x5"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // latest -> blockCapsule1 (head) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("latest", "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); + } catch (Exception e) { + Assert.fail(); + } + + // finalized -> blockCapsule2 (solid), has 1 tx + try { + TransactionResult result = + tronJsonRpc.getTransactionByBlockNumberAndIndex("finalized", "0x0"); + Assert.assertNotNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // non-existent block number returns null (not an error) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("0x1", "0x0"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // pending tag rejected + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("pending", "0x0")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); + + // safe tag rejected (new tag) + Exception safeEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("safe", "0x0")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + + // malformed hex rejected + Exception qqqEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("qqq", "0x0")); + Assert.assertEquals("invalid block number", qqqEx.getMessage()); + + // hex overflows long -> longValueExact rejects + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("0x10000000000000000", "0x0")); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); + } + + /** + * Tests the object-form second argument of eth_call: + * {"blockNumber": "0x..."} or {"blockHash": "0x..."}. + * Only the block-selector parsing is exercised here; the call() + * execution path is covered by other tests. + */ + @Test + public void testGetCallWithBlockObject() { + // neither HashMap nor String -> invalid json request + Exception nonMapEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, new Object())); + Assert.assertEquals("invalid json request", nonMapEx.getMessage()); + + // HashMap without blockNumber/blockHash keys -> invalid json request + Exception emptyMapEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, new HashMap())); + Assert.assertEquals("invalid json request", emptyMapEx.getMessage()); + + // blockNumber with malformed hex -> invalid block number + HashMap badHexParams = new HashMap<>(); + badHexParams.put("blockNumber", "xxx"); + Exception badHexEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, badHexParams)); + Assert.assertEquals("invalid block number", badHexEx.getMessage()); + + // blockNumber overflows long -> invalid block number (longValueExact) + HashMap overflowParams = new HashMap<>(); + overflowParams.put("blockNumber", "0x10000000000000000"); + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, overflowParams)); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); + + // blockNumber points to a non-existent block -> header not found + HashMap missingNumParams = new HashMap<>(); + missingNumParams.put("blockNumber", "0x1"); + Exception missingNumEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, missingNumParams)); + Assert.assertEquals("header not found", missingNumEx.getMessage()); + + // blockHash of an unknown block -> header for hash not found + HashMap missingHashParams = new HashMap<>(); + missingHashParams.put("blockHash", + "0x1111111111111111111111111111111111111111111111111111111111111111"); + Exception missingHashEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, missingHashParams)); + Assert.assertEquals("header for hash not found", missingHashEx.getMessage()); } /** @@ -934,6 +1142,91 @@ public void testNewFilterFinalizedBlock() { Assert.assertEquals("invalid block range params", e5.getMessage()); } + /** + * Tag handling at the RPC boundary for eth_newFilter / eth_getLogs / eth_getFilterLogs. + * - safe/pending are rejected inside LogFilterWrapper (parseBlockNumber -> parseBlockTag) + * - finalized is intercepted by newFilter's upfront guard, but allowed by getLogs + * - getFilterLogs round-trips a filter created with concrete block numbers + */ + @Test + public void testLogFilterTagHandling() { + // eth_newFilter: safe in fromBlock -> TAG_SAFE_SUPPORT_ERROR + Exception newFilterSafeFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("safe", null, null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, newFilterSafeFromEx.getMessage()); + + // eth_newFilter: safe in toBlock + Exception newFilterSafeToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "safe", null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, newFilterSafeToEx.getMessage()); + + // eth_newFilter: pending in fromBlock + Exception newFilterPendingFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("pending", null, null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, newFilterPendingFromEx.getMessage()); + + // eth_newFilter: pending in toBlock + Exception newFilterPendingToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "pending", null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, newFilterPendingToEx.getMessage()); + + // eth_getLogs: safe in fromBlock + Exception getLogsSafeFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest("safe", null, null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, getLogsSafeFromEx.getMessage()); + + // eth_getLogs: safe in toBlock + Exception getLogsSafeToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest(null, "safe", null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, getLogsSafeToEx.getMessage()); + + // eth_getLogs: pending in fromBlock + Exception getLogsPendingFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest("pending", null, null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, getLogsPendingFromEx.getMessage()); + + // eth_getLogs: pending in toBlock + Exception getLogsPendingToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest(null, "pending", null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, getLogsPendingToEx.getMessage()); + + // eth_getLogs: finalized is accepted (resolves to solidBlockNum via parseBlockTag). + // With fromBlock empty, Strategy 2 resolves the range to [solid, solid]. blockCapsule2 + // (solid=4) has no logs in test fixtures, so result must be empty. + try { + LogFilterElement[] result = + tronJsonRpc.getLogs(new FilterRequest(null, "finalized", null, null, null)); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + + // End-to-end happy path for eth_getLogs and eth_getFilterLogs. + // Query range [head, head] = [blockCapsule1, blockCapsule1]. No address/topic filter, + // so LogBlockQuery marks all blocks in the range as candidates. LogMatch then iterates + // blockCapsule1's 2 txs * 2 logs each = 4 LogFilterElements. + String headHex = ByteArray.toJsonHex(blockCapsule1.getNum()); + int expectedLogs = blockCapsule1.getTransactions().size() * 2; + + try { + LogFilterElement[] directResult = + tronJsonRpc.getLogs(new FilterRequest(headHex, headHex, null, null, null)); + Assert.assertEquals(expectedLogs, directResult.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + + try { + String filterIdHex = tronJsonRpc.newFilter( + new FilterRequest(headHex, headHex, null, null, null)); + LogFilterElement[] filterResult = tronJsonRpc.getFilterLogs(filterIdHex); + Assert.assertEquals(expectedLogs, filterResult.length); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + @Test public void testGetBlockReceipts() { @@ -1001,6 +1294,13 @@ public void testGetBlockReceipts() { throw new RuntimeException(e); } + Exception safeReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("safe")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeReceiptsEx.getMessage()); + + Exception overflowReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("0x10000000000000000")); + Assert.assertEquals("invalid block number", overflowReceiptsEx.getMessage()); } @Test diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java new file mode 100644 index 00000000000..4150275c5e2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java @@ -0,0 +1,162 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.Wallet; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; + +/** + * Verify LogFilterWrapper strategies match develop branch behavior. + * + * Four filter strategies based on parameter emptiness (develop branch semantics): + * - Strategy 1: Both fromBlock and toBlock are empty -> (currentMaxBlockNum, Long.MAX_VALUE) + * - Strategy 2: fromBlock empty, toBlock non-empty -> based on toBlock value + * - Strategy 3: fromBlock non-empty, toBlock empty -> (fromBlock, Long.MAX_VALUE) + * - Strategy 4: Both non-empty -> parse both, handle "latest" using snapshot + */ +public class LogFilterWrapperStrategyTest { + + private Wallet mockWallet; + private static final long CURRENT_MAX_BLOCK = 81628775L; + + @Before + public void setUp() { + mockWallet = mock(Wallet.class); + when(mockWallet.getHeadBlockNum()).thenReturn(CURRENT_MAX_BLOCK); + when(mockWallet.getSolidBlockNum()).thenReturn(CURRENT_MAX_BLOCK - 100); + } + + private LogFilterWrapper createFilter(String fromBlock, String toBlock) throws Exception { + FilterRequest request = new FilterRequest(fromBlock, toBlock, null, null, null); + return new LogFilterWrapper(request, CURRENT_MAX_BLOCK, mockWallet, false); + } + + @Test + public void testStrategy1_BothNull() throws Exception { + LogFilterWrapper filter = createFilter(null, null); + assertEquals("fromBlock should be currentMaxBlockNum", CURRENT_MAX_BLOCK, + filter.getFromBlock()); + assertEquals("toBlock should be Long.MAX_VALUE", Long.MAX_VALUE, + filter.getToBlock()); + } + + @Test + public void testStrategy1_BothEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("", ""); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyToHex() throws Exception { + // toBlock = 0x100 = 256 + // fromBlock = min(256, CURRENT_MAX_BLOCK) = 256 + LogFilterWrapper filter = createFilter(null, "0x100"); + assertEquals(256L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyToLatest() throws Exception { + // toBlock = "latest" is treated as Long.MAX_VALUE in Strategy 2 + // fromBlock = min(Long.MAX_VALUE, CURRENT_MAX_BLOCK) = CURRENT_MAX_BLOCK + LogFilterWrapper filter = createFilter(null, "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyStringToHex() throws Exception { + LogFilterWrapper filter = createFilter("", "0x200"); + assertEquals(512L, filter.getFromBlock()); + assertEquals(512L, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromHexToEmpty() throws Exception { + // fromBlock = 0x1 = 1 + // toBlock = Long.MAX_VALUE (tracking future blocks) + LogFilterWrapper filter = createFilter("0x1", null); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromLatestToEmpty() throws Exception { + // fromBlock = "latest" (using snapshot) = currentMaxBlockNum + // toBlock = Long.MAX_VALUE + LogFilterWrapper filter = createFilter("latest", null); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromHexToEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("0x5", ""); + assertEquals(5L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_BothHex() throws Exception { + // fromBlock = 1, toBlock = 256 + LogFilterWrapper filter = createFilter("0x1", "0x100"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy4_BothLatest() throws Exception { + // Both "latest" are non-empty, so Strategy 4. + // fromBlock "latest" -> currentMaxBlockNum (snapshot). toBlock "latest" -> Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("latest", "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromHexToLatest() throws Exception { + // fromBlock = 0x1 (concrete). toBlock = "latest" resolves to Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("0x1", "latest"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromLatestToHexAboveLatest() throws Exception { + // This test requires a toBlock value larger than currentMaxBlockNum + // Using 0x5000000 (83886080) which is > 81628775 + LogFilterWrapper filter = createFilter("latest", "0x5000000"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(83886080L, filter.getToBlock()); + } + + @Test + public void testStrategy4_InvertedRangeThrows() throws Exception { + // fromBlock (0x100 = 256) > toBlock (0x1 = 1) should throw + try { + createFilter("0x100", "0x1"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } + + @Test + public void testStrategy4_LatestGreaterThanSmallBlock_Throws() throws Exception { + // fromBlock = "latest" (currentMaxBlockNum = 81628775) > toBlock (0x100 = 256) should throw + try { + createFilter("latest", "0x100"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } +} From 22e0aa3085736fda7b1395543e887f033176042c Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 30 Apr 2026 11:46:34 +0800 Subject: [PATCH 31/43] feat(jsonrpc): add blockTimestamp to logs and receipts (#6671) --- .../core/services/jsonrpc/TronJsonRpc.java | 13 +++-- .../services/jsonrpc/filters/LogMatch.java | 3 +- .../jsonrpc/types/TransactionReceipt.java | 3 ++ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 38 +++++++++++++-- .../core/jsonrpc/LogMatchExactlyTest.java | 3 ++ .../jsonrpc/TransactionReceiptTest.java | 48 ++++++++++--------- 6 files changed, 79 insertions(+), 29 deletions(-) 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 115df6ef9da..8e7c8615da4 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 @@ -461,10 +461,12 @@ class LogFilterElement { private final String[] topics; @Getter private final boolean removed; + @Getter + private final String blockTimestamp; public LogFilterElement(String blockHash, Long blockNum, String txId, Integer txIndex, String contractAddress, List topicList, String logData, int logIdx, - boolean removed) { + boolean removed, long blockTimestampMs) { logIndex = ByteArray.toJsonHex(logIdx); this.blockNumber = blockNum == null ? null : ByteArray.toJsonHex(blockNum); this.blockHash = blockHash == null ? null : ByteArray.toJsonHex(blockHash); @@ -477,6 +479,7 @@ public LogFilterElement(String blockHash, Long blockNum, String txId, Integer tx topics[i] = ByteArray.toJsonHex(topicList.get(i).getData()); } this.removed = removed; + this.blockTimestamp = ByteArray.toJsonHex(blockTimestampMs / 1000); } @Override @@ -500,12 +503,16 @@ public boolean equals(Object o) { if (!Objects.equals(logIndex, item.logIndex)) { return false; } - return removed == item.removed; + if (removed != item.removed) { + return false; + } + return Objects.equals(blockTimestamp, item.blockTimestamp); } @Override public int hashCode() { - return Objects.hash(blockHash, transactionHash, transactionIndex, logIndex, removed); + return Objects.hash(blockHash, transactionHash, transactionIndex, + logIndex, removed, blockTimestamp); } } 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 cf958d1e2cb..67d229b2948 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 @@ -66,7 +66,8 @@ public static List matchBlock(LogFilter logFilter, long blockN topicList, ByteArray.toHexString(log.getData().toByteArray()), logIndexInBlock, - removed + removed, + transactionInfo.getBlockTimeStamp() ); matchedLog.add(logFilterElement); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java index fd57ec0d9ad..6c22c1560e4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java @@ -35,6 +35,7 @@ public static class TransactionLog { private String data; private String[] topics; private boolean removed = false; + private String blockTimestamp; public TransactionLog() {} } @@ -108,6 +109,7 @@ public TransactionReceipt( // Set logs List logList = new ArrayList<>(); + String blockTimestamp = ByteArray.toJsonHex(blockCapsule.getTimeStamp() / 1000); for (int logIndex = 0; logIndex < txInfo.getLogCount(); logIndex++) { TransactionInfo.Log log = txInfo.getLogList().get(logIndex); TransactionLog transactionLog = new TransactionLog(); @@ -116,6 +118,7 @@ public TransactionReceipt( transactionLog.setTransactionIndex(this.transactionIndex); transactionLog.setBlockHash(this.blockHash); transactionLog.setBlockNumber(this.blockNumber); + transactionLog.setBlockTimestamp(blockTimestamp); byte[] addressByte = convertToTronAddress(log.getAddress().toByteArray()); transactionLog.setAddress(ByteArray.toJsonHexAddress(addressByte)); 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 362cfa0a126..7de494f1783 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -116,11 +116,11 @@ public void init() { blockCapsule0 = BlockUtil.newGenesisBlockCapsule(); blockCapsule1 = new BlockCapsule(LATEST_BLOCK_NUM, Sha256Hash.wrap(ByteString.copyFrom( ByteArray.fromHexString( - "0304f784e4e7bae517bcab94c3e0c9214fb4ac7ff9d7d5a937d1f40031f87b81"))), 1, + "0304f784e4e7bae517bcab94c3e0c9214fb4ac7ff9d7d5a937d1f40031f87b81"))), 1000000, ByteString.copyFromUtf8("testAddress")); blockCapsule2 = new BlockCapsule(LATEST_SOLIDIFIED_BLOCK_NUM, Sha256Hash.wrap( ByteString.copyFrom(ByteArray.fromHexString( - "9938a342238077182498b464ac029222ae169360e540d1fd6aee7c2ae9575a06"))), 1, + "9938a342238077182498b464ac029222ae169360e540d1fd6aee7c2ae9575a06"))), 2000000, ByteString.copyFromUtf8("testAddress")); TransferContract transferContract1 = TransferContract.newBuilder().setAmount(1L) @@ -142,13 +142,15 @@ public void init() { transactionCapsule1 = new TransactionCapsule(transferContract1, ContractType.TransferContract); transactionCapsule1.setBlockNum(blockCapsule1.getNum()); + transactionCapsule1.setTimestamp(blockCapsule1.getTimeStamp()); TransactionCapsule transactionCapsule2 = new TransactionCapsule(transferContract2, ContractType.TransferContract); transactionCapsule2.setBlockNum(blockCapsule1.getNum()); + transactionCapsule2.setTimestamp(blockCapsule1.getTimeStamp()); TransactionCapsule transactionCapsule3 = new TransactionCapsule(transferContract3, ContractType.TransferContract); transactionCapsule3.setBlockNum(blockCapsule2.getNum()); - + transactionCapsule3.setTimestamp(blockCapsule2.getTimeStamp()); blockCapsule1.addTransaction(transactionCapsule1); blockCapsule1.addTransaction(transactionCapsule2); blockCapsule2.addTransaction(transactionCapsule3); @@ -188,6 +190,7 @@ public void init() { TransactionInfoCapsule transactionInfoCapsule = new TransactionInfoCapsule(); transactionInfoCapsule.setId(tx.getTransactionId().getBytes()); transactionInfoCapsule.setBlockNumber(blockCapsule1.getNum()); + transactionInfoCapsule.setBlockTimeStamp(blockCapsule1.getTimeStamp()); transactionInfoCapsule.addAllLog(logs); transactionRetCapsule1.addTransactionInfo(transactionInfoCapsule.getInstance()); }); @@ -199,6 +202,7 @@ public void init() { TransactionInfoCapsule transactionInfoCapsule = new TransactionInfoCapsule(); transactionInfoCapsule.setId(tx.getTransactionId().getBytes()); transactionInfoCapsule.setBlockNumber(blockCapsule2.getNum()); + transactionInfoCapsule.setBlockTimeStamp(blockCapsule2.getTimeStamp()); transactionRetCapsule2.addTransactionInfo(transactionInfoCapsule.getInstance()); }); dbManager.getTransactionRetStore() @@ -1112,6 +1116,30 @@ public void testMethodBlockRange() { } } + @Test + public void testGetLogs() { + try { + LogFilterElement[] logs = tronJsonRpc.getLogs( + new FilterRequest("0x2710", "0x2710", null, null, null)); + Assert.assertTrue(logs.length > 0); + LogFilterElement log = logs[0]; + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), log.getBlockNumber()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getBlockId().toString()), + log.getBlockHash()); + Assert.assertEquals("0x0", log.getLogIndex()); + Assert.assertFalse(log.isRemoved()); + Assert.assertEquals(1, log.getTopics().length); + Assert.assertEquals( + "0x0000000000000000000000000000000000000000000000000000746f70696331", + log.getTopics()[0]); + Assert.assertEquals(ByteArray.toJsonHex("data1".getBytes()), log.getData()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), + log.getBlockTimestamp()); + } catch (Exception e) { + Assert.fail(); + } + } + @Test public void testNewFilterFinalizedBlock() { @@ -1239,6 +1267,10 @@ public void testGetBlockReceipts() { Assert.assertEquals( JSON.toJSONString(transactionReceipt), JSON.toJSONString(transactionReceipt1)); + + Assert.assertTrue(transactionReceipt1.getLogs().length > 0); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), + transactionReceipt1.getLogs()[0].getBlockTimestamp()); } } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { throw new RuntimeException(e); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java index 0f9f125b74e..2151801fc59 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java @@ -37,6 +37,7 @@ private TransactionInfo createTransactionInfo(byte[] address, byte[][] topicArra LogInfo logInfo = new LogInfo(address, topics, data); logList.add(LogInfo.buildLog(logInfo)); builder.addAllLog(logList); + builder.setBlockTimeStamp(1000000L); return builder.build(); } @@ -230,6 +231,8 @@ public void testMatchBlock() { LogFilterElement logFilterElement1 = elementList.get(0); LogFilterElement logFilterElement2 = elementList2.get(0); + Assert.assertEquals("0x3e8", logFilterElement1.getBlockTimestamp()); + Assert.assertEquals("0x3e8", logFilterElement2.getBlockTimestamp()); Assert.assertEquals(logFilterElement1.hashCode(), logFilterElement2.hashCode()); Assert.assertEquals(logFilterElement1, logFilterElement2); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java index a53a32daf45..e9cb8b7e274 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java @@ -32,6 +32,7 @@ public void testTransactionReceipt() throws JsonRpcInternalException { Protocol.TransactionInfo transactionInfo = Protocol.TransactionInfo.newBuilder() .setId(ByteString.copyFrom("1".getBytes())) .setContractAddress(ByteString.copyFrom("address1".getBytes())) + .setBlockTimeStamp(1000000L) .setReceipt(Protocol.ResourceReceipt.newBuilder() .setEnergyUsageTotal(100L) .setResult(Protocol.Transaction.Result.contractResult.DEFAULT) @@ -53,8 +54,11 @@ public void testTransactionReceipt() throws JsonRpcInternalException { Protocol.Block block = Protocol.Block.newBuilder().setBlockHeader( Protocol.BlockHeader.newBuilder().setRawData( - Protocol.BlockHeader.raw.newBuilder().setNumber(1))).addTransactions( - transaction).build(); + Protocol.BlockHeader.raw.newBuilder() + .setNumber(1) + .setTimestamp(1000000L))) + .addTransactions(transaction) + .build(); BlockCapsule blockCapsule = new BlockCapsule(block); long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp()); @@ -65,35 +69,35 @@ public void testTransactionReceipt() throws JsonRpcInternalException { new TransactionReceipt(blockCapsule, transactionInfo, context, energyFee); Assert.assertNotNull(transactionReceipt); - String blockHash = "0x0000000000000001464f071c8a336fd22eb5145dff1b245bda013ec89add8497"; + String blockHash = "0x0000000000000001ba51f50f562758a449ff4a98df4febef89e122c1bb7e1a0c"; // assert basic fields - Assert.assertEquals(transactionReceipt.getBlockHash(), blockHash); - Assert.assertEquals(transactionReceipt.getBlockNumber(), "0x1"); - Assert.assertEquals(transactionReceipt.getTransactionHash(), "0x31"); - Assert.assertEquals(transactionReceipt.getTransactionIndex(), "0x0"); - Assert.assertEquals(transactionReceipt.getCumulativeGasUsed(), ByteArray.toJsonHex(102)); - Assert.assertEquals(transactionReceipt.getGasUsed(), ByteArray.toJsonHex(100)); - Assert.assertEquals(transactionReceipt.getEffectiveGasPrice(), ByteArray.toJsonHex(energyFee)); - Assert.assertEquals(transactionReceipt.getStatus(), "0x1"); + Assert.assertEquals(blockHash, transactionReceipt.getBlockHash()); + Assert.assertEquals("0x1", transactionReceipt.getBlockNumber()); + Assert.assertEquals("0x31", transactionReceipt.getTransactionHash()); + Assert.assertEquals("0x0", transactionReceipt.getTransactionIndex()); + Assert.assertEquals(ByteArray.toJsonHex(102), transactionReceipt.getCumulativeGasUsed()); + Assert.assertEquals(ByteArray.toJsonHex(100), transactionReceipt.getGasUsed()); + Assert.assertEquals(ByteArray.toJsonHex(energyFee), transactionReceipt.getEffectiveGasPrice()); + Assert.assertEquals("0x1", transactionReceipt.getStatus()); // assert contract fields - Assert.assertEquals(transactionReceipt.getFrom(), ByteArray.toJsonHexAddress(new byte[0])); - Assert.assertEquals(transactionReceipt.getTo(), ByteArray.toJsonHexAddress(new byte[0])); + Assert.assertEquals(ByteArray.toJsonHexAddress(new byte[0]), transactionReceipt.getFrom()); + Assert.assertEquals(ByteArray.toJsonHexAddress(new byte[0]), transactionReceipt.getTo()); Assert.assertNull(transactionReceipt.getContractAddress()); // assert logs fields - Assert.assertEquals(transactionReceipt.getLogs().length, 1); - Assert.assertEquals(transactionReceipt.getLogs()[0].getLogIndex(), "0x3"); - Assert.assertEquals( - transactionReceipt.getLogs()[0].getBlockHash(), blockHash); - Assert.assertEquals(transactionReceipt.getLogs()[0].getBlockNumber(), "0x1"); - Assert.assertEquals(transactionReceipt.getLogs()[0].getTransactionHash(), "0x31"); - Assert.assertEquals(transactionReceipt.getLogs()[0].getTransactionIndex(), "0x0"); + Assert.assertEquals(1, transactionReceipt.getLogs().length); + Assert.assertEquals("0x3", transactionReceipt.getLogs()[0].getLogIndex()); + Assert.assertEquals(blockHash, transactionReceipt.getLogs()[0].getBlockHash()); + Assert.assertEquals("0x1", transactionReceipt.getLogs()[0].getBlockNumber()); + Assert.assertEquals("0x31", transactionReceipt.getLogs()[0].getTransactionHash()); + Assert.assertEquals("0x0", transactionReceipt.getLogs()[0].getTransactionIndex()); + Assert.assertEquals("0x3e8", transactionReceipt.getLogs()[0].getBlockTimestamp()); // assert default fields Assert.assertNull(transactionReceipt.getRoot()); - Assert.assertEquals(transactionReceipt.getType(), "0x0"); - Assert.assertEquals(transactionReceipt.getLogsBloom(), ByteArray.toJsonHex(new byte[256])); + Assert.assertEquals("0x0", transactionReceipt.getType()); + Assert.assertEquals(ByteArray.toJsonHex(new byte[256]), transactionReceipt.getLogsBloom()); } } From d72ba4c80fb6ceed6026db984ad6daa3482da30e Mon Sep 17 00:00:00 2001 From: "wayne.zhang" <43207186+waynercheung@users.noreply.github.com> Date: Thu, 7 May 2026 19:49:04 +0800 Subject: [PATCH 32/43] fix(jsonrpc): accept "input" as alias for "data" in call args (#6722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(jsonrpc): accept "input" as alias for "data" in call args Closes #6517. JSON-RPC requests using the execution-apis field name `input` were rejected with UnrecognizedPropertyException, blocking spec-compliant clients -- notably go-ethereum's ethclient since ethereum/go-ethereum#28078, which only emits `input`. CallArguments and BuildArguments now declare both fields. A new resolveData() does pure precedence resolution -- `input` wins over `data` -- mirroring go-ethereum's TransactionArgs.data(). Five call sites in TronJsonRpcImpl use the resolver instead of getData(). The verb-prefix name (not getXxx) keeps Jackson and FastJSON's JavaBean introspection from picking the method up as a `resolveData` wire property; two serialisation tests pin this as a regression guard. Hex validation is mode-driven via JsonRpcApiUtil.requireValidHex (HexMode.STRICT / HexMode.LENIENT): - `input` (new field) is STRICT: follows the execution-apis BYTES schema -- requires `0x` prefix and even length; "" is accepted as empty bytes per geth's hexutil.Bytes.UnmarshalText. - `data` retains LENIENT parsing via ByteArray.fromHexString for backward compatibility with existing callers (e.g. BuildTransactionTest.testCreateSmartContract uses bare-hex bytecode). Conflict between `input` and `data` (both set, not equal) is detected on the build path at BuildArguments.getContractType(), mirroring geth's data() / setDefaults split. The error message matches go-ethereum's setDefaults wording verbatim. Comparison is byte-level so case differences are not flagged. The query path (CallArguments.resolveData()) stays lenient -- input wins silently. Tested by 52 unit tests including Jackson/FastJSON serialisation safety guards; BuildTransactionTest.testCreateSmartContract still passes; checkstyle clean. * refactor(jsonrpc): extract conflict error message to constant Hoist the input/data conflict error string in BuildArguments to a private static final field CONFLICT_ERR_MSG, replacing the inline literal in validateCallDataConflict. The wording mirrors go-ethereum's setDefaults verbatim and is a cross-project contract — external EVM tooling may pattern-match the string. The constant's javadoc records this so future refactors do not drift from the geth wording. --- .../core/services/jsonrpc/JsonRpcApiUtil.java | 44 +++ .../services/jsonrpc/TronJsonRpcImpl.java | 14 +- .../jsonrpc/types/BuildArguments.java | 71 ++++- .../services/jsonrpc/types/CallArguments.java | 30 +- .../services/jsonrpc/BuildArgumentsTest.java | 285 +++++++++++++++++- .../services/jsonrpc/CallArgumentsTest.java | 205 ++++++++++++- 6 files changed, 620 insertions(+), 29 deletions(-) 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 08c1068e3a2..70673d2148a 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 @@ -442,6 +442,50 @@ public static boolean paramQuantityIsNull(String quantity) { return StringUtils.isEmpty(quantity) || quantity.equals("0x0"); } + /** + * Validation mode for {@link #requireValidHex}. + */ + public enum HexMode { + /** + * Execution-apis BYTES schema: requires {@code 0x} prefix and + * even total length; {@code ""} is accepted as empty bytes per + * geth's {@code hexutil.Bytes.UnmarshalText}. + */ + STRICT, + /** + * {@link ByteArray#fromHexString}'s lenient parsing: accepts bare + * hex and odd-length input. Kept for backward compatibility. + */ + LENIENT + } + + /** + * Throws if {@code value} is not parseable hex under the given + * {@code mode}. {@code null} is treated as absent and returns + * silently. {@code fieldName} is used only in error messages. + */ + public static void requireValidHex(String fieldName, String value, HexMode mode) + throws JsonRpcInvalidParamsException { + if (value == null) { + return; + } + if (mode == HexMode.STRICT) { + if (value.isEmpty()) { + return; + } + if (!value.startsWith("0x") || value.length() % 2 != 0) { + throw new JsonRpcInvalidParamsException( + "invalid hex string for \"" + fieldName + "\""); + } + } + try { + ByteArray.fromHexString(value); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException( + "invalid hex string for \"" + fieldName + "\""); + } + } + public static long parseQuantityValue(String value) throws JsonRpcInvalidParamsException { long callValue = 0L; 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 72fc579aa56..ea8f15cd088 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 @@ -645,7 +645,7 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept estimateEnergy(ownerAddress, contractAddress, args.parseValue(), - ByteArray.fromHexString(args.getData()), + ByteArray.fromHexString(args.resolveData()), trxExtBuilder, retBuilder, estimateBuilder); @@ -653,7 +653,7 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept callTriggerConstantContract(ownerAddress, contractAddress, args.parseValue(), - ByteArray.fromHexString(args.getData()), + ByteArray.fromHexString(args.resolveData()), trxExtBuilder, retBuilder); } @@ -1007,7 +1007,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); return call(addressData, contractAddressData, transactionCall.parseValue(), - ByteArray.fromHexString(transactionCall.getData())); + ByteArray.fromHexString(transactionCall.resolveData())); } @Override @@ -1114,7 +1114,8 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, smartBuilder.setOriginAddress(ByteString.copyFrom(ownerAddress)); // bytecode + parameter - smartBuilder.setBytecode(ByteString.copyFrom(ByteArray.fromHexString(args.getData()))); + smartBuilder.setBytecode( + ByteString.copyFrom(ByteArray.fromHexString(args.resolveData()))); if (StringUtils.isNotEmpty(args.getName())) { smartBuilder.setName(args.getName()); @@ -1159,8 +1160,9 @@ private TransactionJson buildTriggerSmartContractTransaction(byte[] ownerAddress build.setOwnerAddress(ByteString.copyFrom(ownerAddress)) .setContractAddress(ByteString.copyFrom(contractAddress)); - if (StringUtils.isNotEmpty(args.getData())) { - build.setData(ByteString.copyFrom(ByteArray.fromHexString(args.getData()))); + String callData = args.resolveData(); + if (StringUtils.isNotEmpty(callData)) { + build.setData(ByteString.copyFrom(ByteArray.fromHexString(callData))); } else { build.setData(ByteString.copyFrom(new byte[0])); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java index 490219a13d9..ef4e958ae44 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java @@ -4,8 +4,10 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramQuantityIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramStringIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; import com.google.protobuf.ByteString; +import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,9 +15,11 @@ import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil.HexMode; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; @@ -24,6 +28,16 @@ @ToString public class BuildArguments { + /** + * Conflict error message wording. Mirrors go-ethereum's + * {@code setDefaults} verbatim — external EVM tooling may + * pattern-match this string. Do not change the wording without + * coordinating with downstream consumers. + */ + private static final String CONFLICT_ERR_MSG = + "both \"data\" and \"input\" are set and not equal. " + + "Please use \"input\" to pass transaction call data"; + @Getter @Setter private String from; @@ -44,6 +58,9 @@ public class BuildArguments { private String data; @Getter @Setter + private String input; + @Getter + @Setter private String nonce = ""; //not used @Getter @@ -83,16 +100,50 @@ public BuildArguments(CallArguments args) { gasPrice = args.getGasPrice(); value = args.getValue(); data = args.getData(); + input = args.getInput(); + } + + /** + * Returns {@code input} if non-null, else {@code data}. Pure + * precedence resolution, mirroring go-ethereum's + * {@code TransactionArgs.data()}. + * + *

Both fields are first validated by + * {@link org.tron.core.services.jsonrpc.JsonRpcApiUtil#requireValidHex} + * — strict for {@code input}, lenient for {@code data} (see that + * method for the rules). + * + *

Conflict between {@code input} and {@code data} is not checked + * here. Build-path callers must route through + * {@link #getContractType(Wallet)} for the geth-equivalent + * {@code setDefaults} enforcement. + * + *

Java callers using positional constructors should pass + * {@code null} (not {@code ""}) for unset {@code input}. + * + *

Verb-prefix name (not {@code getXxx}) keeps Jackson and + * FastJSON's JavaBean introspection from invoking it during + * serialisation; two regression tests per DTO pin this invariant. + */ + public String resolveData() throws JsonRpcInvalidParamsException { + requireValidHex("input", input, HexMode.STRICT); + requireValidHex("data", data, HexMode.LENIENT); + return input != null ? input : data; } public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + // Fail fast on bad hex / conflict before the state lookup; + // calldataEquals relies on resolveData() having validated hex first. + String resolvedData = resolveData(); + validateCallDataConflict(); + ContractType contractType; // to is null if (paramStringIsNull(to)) { // data is null - if (paramStringIsNull(data)) { + if (paramStringIsNull(resolvedData)) { throw new JsonRpcInvalidRequestException("invalid json request"); } @@ -136,4 +187,22 @@ private boolean availableTransferAsset() { return tokenId > 0 && tokenValue > 0 && paramQuantityIsNull(value); } + /** + * Throws when both fields decode to non-equal bytes. Wording matches + * geth's setDefaults so existing tooling can detect the error string. + */ + private void validateCallDataConflict() throws JsonRpcInvalidParamsException { + if (input != null && data != null && !calldataEquals(input, data)) { + throw new JsonRpcInvalidParamsException(CONFLICT_ERR_MSG); + } + } + + /** + * Byte-level equality, so {@code "0xDEAD"} equals {@code "0xdead"}. Both + * args must have passed {@code requireValidHex} first. + */ + private static boolean calldataEquals(String a, String b) { + return Arrays.equals(ByteArray.fromHexString(a), ByteArray.fromHexString(b)); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java index 70edd1ad94f..1715636a2a4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java @@ -3,6 +3,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramStringIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; import com.google.protobuf.ByteString; import lombok.AllArgsConstructor; @@ -15,6 +16,7 @@ import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil.HexMode; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; @@ -43,21 +45,41 @@ public class CallArguments { private String data; @Getter @Setter + private String input; + @Getter + @Setter private String nonce; // not used + /** + * Returns {@code input} if non-null, else {@code data}. Pure + * precedence resolution, mirroring go-ethereum's + * {@code TransactionArgs.data()}; no conflict check on the query + * path (matches geth's {@code ToMessage}). See + * {@link BuildArguments#resolveData()} for the rationale on + * naming, validation split, and serialiser interaction. + */ + public String resolveData() throws JsonRpcInvalidParamsException { + requireValidHex("input", input, HexMode.STRICT); + requireValidHex("data", data, HexMode.LENIENT); + return input != null ? input : data; + } + /** * just support TransferContract, CreateSmartContract and TriggerSmartContract * */ public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { - ContractType contractType; - // from or to is null if (paramStringIsNull(from)) { throw new JsonRpcInvalidRequestException("invalid json request"); - } else if (paramStringIsNull(to)) { + } + // Fail fast on bad hex before the state lookup. + String resolvedData = resolveData(); + + ContractType contractType; + if (paramStringIsNull(to)) { // data is null - if (paramStringIsNull(data)) { + if (paramStringIsNull(resolvedData)) { throw new JsonRpcInvalidRequestException("invalid json request"); } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 26699bc63f6..5ad2c85d181 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.ObjectMapper; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Before; @@ -29,9 +30,9 @@ public class BuildArgumentsTest extends BaseTest { public void initBuildArgs() { buildArguments = new BuildArguments( "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0",9L,10000L,"",10L, - 2000L,"args",1,"",true); + "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", + "", "", "0", 9L, 10000L, "", 10L, + 2000L, "args", 1, "", true); } @@ -39,15 +40,13 @@ public void initBuildArgs() { public void testBuildArgument() { CallArguments callArguments = new CallArguments( "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0"); - BuildArguments buildArguments = new BuildArguments(callArguments); - Assert.assertEquals(buildArguments.getFrom(), - "0x0000000000000000000000000000000000000000"); - Assert.assertEquals(buildArguments.getTo(), - "0x0000000000000000000000000000000000000001"); - Assert.assertEquals(buildArguments.getGas(), "0x10"); - Assert.assertEquals(buildArguments.getGasPrice(), "0.01"); + "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", + "", "", "0"); + BuildArguments args = new BuildArguments(callArguments); + Assert.assertEquals("0x0000000000000000000000000000000000000000", args.getFrom()); + Assert.assertEquals("0x0000000000000000000000000000000000000001", args.getTo()); + Assert.assertEquals("0x10", args.getGas()); + Assert.assertEquals("0.01", args.getGasPrice()); } @Test @@ -55,19 +54,275 @@ public void testGetContractType() throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { Protocol.Transaction.Contract.ContractType contractType = buildArguments.getContractType(wallet); - Assert.assertEquals(contractType, Protocol.Transaction.Contract.ContractType.TransferContract); + Assert.assertEquals(Protocol.Transaction.Contract.ContractType.TransferContract, contractType); } @Test public void testParseValue() throws JsonRpcInvalidParamsException { long value = buildArguments.parseValue(); - Assert.assertEquals(value, 256L); + Assert.assertEquals(256L, value); } @Test public void testParseGas() throws JsonRpcInvalidParamsException { long gas = buildArguments.parseGas(); - Assert.assertEquals(gas, 16L); + Assert.assertEquals(16L, gas); + } + + @Test + public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOnly_returnsData() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0xcafebabe"); + Assert.assertEquals("0xcafebabe", args.resolveData()); + } + + @Test + public void resolveData_bothPresentSame_returnsValue() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0xfeedface"); + args.setInput("0xfeedface"); + Assert.assertEquals("0xfeedface", args.resolveData()); + } + + /** Pins that "0x" on both sides decodes to []==[] and is not a conflict. */ + @Test + public void resolveData_bothZeroX_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0x"); + args.setData("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_inputZeroXOnly_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_dataZeroXOnly_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_caseDifference_returnsInput() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0xDEADbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0xDEADbeef", args.resolveData()); + } + + /** + * Pins geth-equivalent semantics: empty string is presence with + * empty bytes, so paired with non-empty data the byte values differ + * and the build path raises the geth setDefaults conflict at the + * {@code getContractType()} entry point. + */ + @Test + public void getContractType_inputEmptyDataNonEmpty_throwsConflict() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput(""); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + } + + /** + * Wording matches go-ethereum's setDefaults so existing tooling can + * detect the error string. + */ + @Test + public void getContractType_inputAndDataConflict_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + + JsonRpcInvalidParamsException ex = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + Assert.assertTrue( + "error message should match go-ethereum's wording: " + ex.getMessage(), + ex.getMessage().contains("both \"data\" and \"input\" are set and not equal")); + } + + @Test + public void getContractType_inputZeroXDataNonEmpty_throwsConflict() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0x"); + args.setData("0xdeadbeef"); + + Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + } + + @Test + public void getContractType_inputAndDataEqual_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + /** Reproduces issue #6517 contract-creation symptom on the build path. */ + @Test + public void getContractType_createSmartContractViaInput_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + @Test + public void copyConstructor_preservesBothInputAndData() { + CallArguments src = new CallArguments(); + src.setFrom("0x0000000000000000000000000000000000000001"); + src.setData("0xcafebabe"); + src.setInput("0xdeadbeef"); + + BuildArguments copy = new BuildArguments(src); + Assert.assertEquals("0xcafebabe", copy.getData()); + Assert.assertEquals("0xdeadbeef", copy.getInput()); + } + + @Test + public void copyConstructor_propagatesConflictToBuildPath() { + CallArguments src = new CallArguments(); + src.setData("0xcafebabe"); + src.setInput("0xdeadbeef"); + + BuildArguments copy = new BuildArguments(src); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> copy.getContractType(wallet)); + } + + @Test + public void deserializeWithInputField_succeedsAndResolvesToInput() throws Exception { + String json = "{\"from\":\"0x0000000000000000000000000000000000000001\"," + + "\"to\":\"0x0000000000000000000000000000000000000002\"," + + "\"input\":\"0xdeadbeef\"}"; + BuildArguments args = new ObjectMapper().readValue(json, BuildArguments.class); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + Assert.assertEquals("0xdeadbeef", args.getInput()); + Assert.assertNull(args.getData()); + } + + /** + * Regression guard: a future {@code getXxx} rename would expose + * {@code resolveData} as a wire property and risk throwing during + * serialisation. + */ + @Test + public void jacksonSerialize_doesNotExposeResolveDataOrThrowOnConflict() + throws Exception { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); // conflicting bytes, would throw if resolveData() were invoked + String json = new ObjectMapper().writeValueAsString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Same guarantee for FastJSON, which also discovers bean getters. */ + @Test + public void fastjsonSerialize_doesNotExposeResolveDataOrThrowOnConflict() { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + String json = com.alibaba.fastjson.JSON.toJSONString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Validates the loser field too, not only the precedence winner. */ + @Test + public void resolveData_inputValidDataMalformed_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataValid_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xzz"); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataAbsent_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_dataMalformedInputAbsent_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code input} is the new spec-aligned field: missing {@code 0x} prefix + * is rejected per the execution-apis BYTES schema. + */ + @Test + public void resolveData_inputNoPrefix_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("deadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputOddLength_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0x123"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code data} is the legacy field: bare hex (no {@code 0x} prefix) + * stays accepted for backward compatibility with existing callers + * (e.g. BuildTransactionTest.testCreateSmartContract). + */ + @Test + public void resolveData_dataNoPrefix_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("deadbeef"); + Assert.assertEquals("deadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOddLength_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0x123"); + Assert.assertEquals("0x123", args.resolveData()); } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java index 2148e1a2fe0..66fb8e0a0c7 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.ObjectMapper; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Before; @@ -26,9 +27,11 @@ public class CallArgumentsTest extends BaseTest { @Before public void init() { - callArguments = new CallArguments("0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0"); + callArguments = new CallArguments( + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000001", + "0x10", "0.01", "0x100", + "", "", "0"); } @Test @@ -44,4 +47,200 @@ public void testParseValue() throws JsonRpcInvalidParamsException { Assert.assertEquals(256L, value); } + @Test + public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOnly_returnsData() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xcafebabe"); + Assert.assertEquals("0xcafebabe", args.resolveData()); + } + + @Test + public void resolveData_bothPresentSame_returnsValue() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xfeedface"); + args.setInput("0xfeedface"); + Assert.assertEquals("0xfeedface", args.resolveData()); + } + + @Test + public void resolveData_bothPresentDifferent_inputWinsNoError() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xcafebabe"); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_inputIsZeroX_dataNonEmpty_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0x"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_dataIsZeroX_inputNonEmpty_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0x"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_caseDifference_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xDEADbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0xDEADbeef", args.resolveData()); + } + + /** Pins geth-equivalent semantics: "" is presence, wins over data by precedence. */ + @Test + public void resolveData_inputEmpty_dataNonEmpty_inputWinsAsEmpty() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput(""); + args.setData("0xdeadbeef"); + Assert.assertEquals("", args.resolveData()); + } + + @Test + public void resolveData_neitherPresent_returnsNull() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + Assert.assertNull(args.resolveData()); + } + + /** Validates the loser field too, not only the precedence winner. */ + @Test + public void resolveData_inputValidDataMalformed_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataValid_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xzz"); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataAbsent_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_dataMalformedInputAbsent_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code input} is the new spec-aligned field: missing {@code 0x} prefix + * is rejected per the execution-apis BYTES schema. + */ + @Test + public void resolveData_inputNoPrefix_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("deadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputOddLength_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0x123"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code data} is the legacy field: bare hex (no {@code 0x} prefix) + * stays accepted for backward compatibility with existing callers + * (e.g. BuildTransactionTest.testCreateSmartContract). + */ + @Test + public void resolveData_dataNoPrefix_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("deadbeef"); + Assert.assertEquals("deadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOddLength_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0x123"); + Assert.assertEquals("0x123", args.resolveData()); + } + + /** Reproduces issue #6517 contract-creation symptom. */ + @Test + public void getContractType_createSmartContractViaInput_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + /** Reproduces issue #6517 Jackson parse-error symptom. */ + @Test + public void deserializeWithInputField_succeedsAndResolvesToInput() throws Exception { + String json = "{\"from\":\"0x0000000000000000000000000000000000000001\"," + + "\"to\":\"0x0000000000000000000000000000000000000002\"," + + "\"input\":\"0xdeadbeef\"}"; + CallArguments args = new ObjectMapper().readValue(json, CallArguments.class); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + Assert.assertEquals("0xdeadbeef", args.getInput()); + Assert.assertNull(args.getData()); + } + + /** + * Regression guard: a future {@code getXxx} rename would expose + * {@code resolveData} as a wire property and risk throwing during + * serialisation. + */ + @Test + public void jacksonSerialize_doesNotExposeResolveDataOrThrowOnConflict() + throws Exception { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); // would throw conflict in build path + String json = new ObjectMapper().writeValueAsString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Same guarantee for FastJSON, which also discovers bean getters. */ + @Test + public void fastjsonSerialize_doesNotExposeResolveDataOrThrowOnConflict() { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + String json = com.alibaba.fastjson.JSON.toJSONString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + } From 26d1435eca8426bc27e4b3688fb3746fef885191 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Thu, 7 May 2026 19:50:26 +0800 Subject: [PATCH 33/43] build(crypto,rpc,http,event): bump bundled libs for security fixes (#6747) 1. bump bcprov-jdk18on from 1.79 to 1.84 fix CVE-2026-5598 2. bump jetty from 9.4.57 to 9.4.58 fix CVE-2025-5115 3. bump pf4j from 3.10.0 to 3.14.1 fix CVE-2025-70952 4. bump grpc-java from 1.75 to 1.81 fix CVE-2026-33871 --- build.gradle | 4 +- framework/build.gradle | 6 +- gradle/verification-metadata.xml | 480 ++++++++++++++++--------------- plugins/build.gradle | 2 +- protocol/build.gradle | 3 +- 5 files changed, 260 insertions(+), 235 deletions(-) diff --git a/build.gradle b/build.gradle index 12a0622db99..4ceebadb158 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ ext.archInfo = [ // https://github.com/grpc/grpc-java/issues/7690 // https://github.com/grpc/grpc-java/pull/12319, Add support for macOS aarch64 with universal binary // https://github.com/grpc/grpc-java/pull/11371 , 1.64.x is not supported CentOS 7. - ProtocGenVersion: isArm64 && isMac ? '1.76.0' : '1.60.0' + ProtocGenVersion: isArm64 || isMac ? '1.81.0' : '1.60.0' ], VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions" ] @@ -90,7 +90,7 @@ subprojects { implementation group: 'org.apache.commons', name: 'commons-math', version: '2.2' implementation "org.apache.commons:commons-collections4:4.1" implementation group: 'joda-time', name: 'joda-time', version: '2.3' - implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.79' + implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.84' compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34' diff --git a/framework/build.gradle b/framework/build.gradle index d884b6a7c49..1aa266da3cd 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -42,8 +42,8 @@ dependencies { implementation group: 'io.dropwizard.metrics', name: 'metrics-core', version: '3.1.2' implementation group: 'com.github.davidb', name: 'metrics-influxdb', version: '0.8.2' // http - implementation 'org.eclipse.jetty:jetty-server:9.4.57.v20241219' - implementation 'org.eclipse.jetty:jetty-servlet:9.4.57.v20241219' + implementation 'org.eclipse.jetty:jetty-server:9.4.58.v20250814' + implementation 'org.eclipse.jetty:jetty-servlet:9.4.58.v20250814' implementation 'com.alibaba:fastjson:1.2.83' // end http @@ -53,7 +53,7 @@ dependencies { // https://mvnrepository.com/artifact/javax.portlet/portlet-api compileOnly group: 'javax.portlet', name: 'portlet-api', version: '3.0.1' - implementation (group: 'org.pf4j', name: 'pf4j', version: '3.10.0') { + implementation (group: 'org.pf4j', name: 'pf4j', version: '3.14.1') { exclude group: "org.slf4j", module: "slf4j-api" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 86880157f35..8ef39e7669b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -289,12 +289,12 @@ - - - + + + - - + + @@ -315,12 +315,12 @@ - - - + + + - - + + @@ -347,12 +347,12 @@ - - - + + + - - + + @@ -363,9 +363,9 @@ - - - + + + @@ -386,12 +386,12 @@ - - - + + + - - + + @@ -404,9 +404,9 @@ - - - + + + @@ -417,12 +417,12 @@ - - - + + + - - + + @@ -467,15 +467,15 @@ - - - + + + - - + + - - + + @@ -508,14 +508,14 @@ - - - + + + - - - + + + @@ -539,12 +539,12 @@ - - - + + + - - + + @@ -888,194 +888,191 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - + + + + + + - - + + + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -1654,12 +1651,12 @@ - - - + + + - - + + @@ -1699,12 +1696,12 @@ - - - + + + - - + + @@ -1712,9 +1709,9 @@ - - - + + + @@ -1722,9 +1719,9 @@ - - - + + + @@ -1751,65 +1748,65 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -2012,6 +2009,17 @@ + + + + + + + + + + + @@ -2044,6 +2052,22 @@ + + + + + + + + + + + + + + + + @@ -2168,17 +2192,17 @@ - - - + + + - - + + - - - + + + diff --git a/plugins/build.gradle b/plugins/build.gradle index 2e358a884a3..fc9ef5e00d6 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' implementation group: 'me.tongfei', name: 'progressbar', version: '0.9.3' - implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.79' + implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.84' if (rootProject.archInfo.isArm64) { testRuntimeOnly group: 'org.fusesource.hawtjni', name: 'hawtjni-runtime', version: '1.18' // for test implementation project(":platform") diff --git a/protocol/build.gradle b/protocol/build.gradle index 04d970b59db..0ce01a9bfb8 100644 --- a/protocol/build.gradle +++ b/protocol/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'com.google.protobuf' apply from: 'protoLint.gradle' def protobufVersion = '3.25.8' -def grpcVersion = '1.75.0' +// keep same version as protoc-gen-grpc-java for arm64 or macOS, see rootProject.archInfo.requires.ProtocGenVersion +def grpcVersion = '1.81.0' dependencies { api group: 'com.google.protobuf', name: 'protobuf-java', version: protobufVersion From 5632991ee61c9c34b365de67e9cb999770e9da81 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Thu, 7 May 2026 20:18:38 +0800 Subject: [PATCH 34/43] feat(vm,consensus): harden resource calculations(TIP-833) (#6721) * feat(proposal): add ALLOW_HARDEN_RESOURCE_CALCULATION (code 97) * feat(vm,consensus): harden resource calculations(TIP-833) * fix(vm): correct parens in RepositoryImpl.calculateGlobalEnergyLimit * feat(vm): use TRX_PRECISION --- .../org/tron/core/utils/ProposalUtil.java | 19 +- .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../core/vm/repository/RepositoryImpl.java | 61 ++- .../org/tron/core/db/BandwidthProcessor.java | 10 +- .../org/tron/core/db/EnergyProcessor.java | 40 +- .../org/tron/core/db/ResourceProcessor.java | 138 ++++++- .../core/store/DynamicPropertiesStore.java | 18 + .../org/tron/core/vm/config/VMConfig.java | 10 + .../src/main/java/org/tron/core/Wallet.java | 5 + .../tron/core/consensus/ProposalService.java | 5 + .../core/actuator/utils/ProposalUtilTest.java | 44 +++ .../db/CalculateGlobalLimitHardenTest.java | 346 ++++++++++++++++++ .../core/db/ResourceProcessorHardenTest.java | 281 ++++++++++++++ .../repository/RepositoryImplHardenTest.java | 280 ++++++++++++++ 14 files changed, 1226 insertions(+), 32 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java create mode 100644 framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java create mode 100644 framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index cd42d7a9010..6b297883fbd 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -886,6 +886,22 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]"); + } + if (dynamicPropertiesStore.getAllowHardenResourceCalculation() == 1) { + throw new ContractValidateException( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, " + + "no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1"); + } + break; + } default: break; } @@ -971,7 +987,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_BLOB(89), // 0, 1 PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 - ALLOW_TVM_OSAKA(96); // 0, 1 + ALLOW_TVM_OSAKA(96), // 0, 1 + ALLOW_HARDEN_RESOURCE_CALCULATION(97); // 0, 1 private long code; diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index e099101912b..881eb861bea 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -46,6 +46,7 @@ public static void load(StoreFactory storeFactory) { VMConfig.initAllowTvmBlob(ds.getAllowTvmBlob()); VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); + VMConfig.initAllowHardenResourceCalculation(ds.getAllowHardenResourceCalculation()); } } } diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 9de7c0691ba..62e7ce6ec08 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -8,6 +8,7 @@ import com.google.common.collect.HashBasedTable; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.util.HashMap; import java.util.HashSet; import java.util.Optional; @@ -17,6 +18,7 @@ import org.bouncycastle.util.Strings; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; @@ -223,7 +225,7 @@ public Pair getAccountEnergyUsageBalanceAndRestoreSeconds(AccountCap long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getTotalEnergyWeight(); - long balance = (long) ((double) newEnergyUsage * totalEnergyWeight / totalEnergyLimit * TRX_PRECISION); + long balance = usageToBalance(newEnergyUsage, totalEnergyWeight, totalEnergyLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } @@ -246,11 +248,22 @@ public Pair getAccountNetUsageBalanceAndRestoreSeconds(AccountCapsul long totalNetLimit = getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = getTotalNetWeight(); - long balance = (long) ((double) newNetUsage * totalNetWeight / totalNetLimit * TRX_PRECISION); + long balance = usageToBalance(newNetUsage, totalNetWeight, totalNetLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } + private long usageToBalance(long usage, long totalWeight, long totalLimit) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(totalWeight)) + .multiply(BigInteger.valueOf(TRX_PRECISION)) + .divide(BigInteger.valueOf(totalLimit)) + .longValueExact(); + } + return (long) ((double) usage * totalWeight / totalLimit * TRX_PRECISION); + } + @Override public AssetIssueCapsule getAssetIssue(byte[] tokenId) { byte[] tokenIdWithoutLeadingZero = ByteUtil.stripLeadingZeroes(tokenId); @@ -896,8 +909,19 @@ private long recover(long lastUsage, long lastTime, long now, long personalWindo } private long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenResourceCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -917,21 +941,46 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)) + .longValueExact(); + } return usage * windowSize / precision; } + private boolean hardenResourceCalculation() { + return VMConfig.allowHardenResourceCalculation(); + } + public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { long frozeBalance = accountCapsule.getAllFrozenBalanceForEnergy(); - if (frozeBalance < 1_000_000L) { + if (frozeBalance < TRX_PRECISION) { return 0; } - long energyWeight = frozeBalance / 1_000_000L; + long energyWeight = frozeBalance / TRX_PRECISION; long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getDynamicPropertiesStore().getTotalEnergyWeight(); assert totalEnergyWeight > 0; + if (hardenResourceCalculation()) { + return BigInteger.valueOf(energyWeight) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + } return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } diff --git a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java index 2488686bfb0..ece16b25819 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -437,7 +437,6 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (frozeBalance < TRX_PRECISION) { return 0; } - long netWeight = frozeBalance / TRX_PRECISION; long totalNetLimit = chainBaseManager.getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = chainBaseManager.getDynamicPropertiesStore().getTotalNetWeight(); if (dynamicPropertiesStore.allowNewReward() && totalNetWeight <= 0) { @@ -446,16 +445,23 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalNetLimit, totalNetWeight); + } + long netWeight = frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } public long calculateGlobalNetLimitV2(long frozeBalance) { - double netWeight = (double) frozeBalance / TRX_PRECISION; long totalNetLimit = dynamicPropertiesStore.getTotalNetLimit(); long totalNetWeight = dynamicPropertiesStore.getTotalNetWeight(); if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalNetLimit, totalNetWeight); + } + double netWeight = (double) frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } diff --git a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java index 30d778d0990..0c429178636 100644 --- a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java @@ -5,6 +5,7 @@ import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; import org.tron.common.parameter.CommonParameter; import org.tron.core.capsule.AccountCapsule; @@ -71,17 +72,20 @@ public void updateAdaptiveTotalEnergyLimit() { long result; if (totalEnergyAverageUsage > targetTotalEnergyLimit) { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + ">" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR); } else { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + "<" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR); } + long upperBound = hardenCalculation() + ? BigInteger.valueOf(totalEnergyLimit).multiply(BigInteger.valueOf( + dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier())).longValueExact() + : totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(); result = min(max(result, totalEnergyLimit, this.disableJavaLangMath()), - totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(), - this.disableJavaLangMath()); + upperBound, this.disableJavaLangMath()); dynamicPropertiesStore.saveTotalEnergyCurrentLimit(result); logger.debug("Adjust totalEnergyCurrentLimit, old: {}, new: {}.", @@ -147,7 +151,6 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { return 0; } - long energyWeight = frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (dynamicPropertiesStore.allowNewReward() && totalEnergyWeight <= 0) { @@ -155,16 +158,23 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { } else { assert totalEnergyWeight > 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + long energyWeight = frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } public long calculateGlobalEnergyLimitV2(long frozeBalance) { - double energyWeight = (double) frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (totalEnergyWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + double energyWeight = (double) frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } @@ -184,7 +194,15 @@ private long getHeadSlot() { return getHeadSlot(dynamicPropertiesStore); } - + private long scaleByRate(long value, long numerator, long denominator) { + if (hardenCalculation()) { + return BigInteger.valueOf(value) + .multiply(BigInteger.valueOf(numerator)) + .divide(BigInteger.valueOf(denominator)) + .longValueExact(); + } + return value * numerator / denominator; + } } diff --git a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java index 7e170f9dab5..6706c430084 100644 --- a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java @@ -3,8 +3,11 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.math.Maths.round; import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; import static org.tron.core.config.Parameter.ChainConstant.WINDOW_SIZE_PRECISION; +import java.math.BigInteger; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.Commons; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.TransactionCapsule; @@ -45,8 +48,19 @@ protected long increase(long lastUsage, long usage, long lastTime, long now) { } protected long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -75,8 +89,20 @@ public long increase(AccountCapsule accountCapsule, ResourceCode resourceCode, return increaseV2(accountCapsule, resourceCode, lastUsage, usage, lastTime, now); } long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -108,8 +134,20 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, long lastUsage, long usage, long lastTime, long now) { long oldWindowSizeV2 = accountCapsule.getWindowSizeV2(resourceCode); long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -130,8 +168,19 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, } long remainWindowSize = oldWindowSizeV2 - (now - lastTime) * WINDOW_SIZE_PRECISION; - long newWindowSize = divideCeil( - remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, newUsage); + long newWindowSize; + if (hardenCalculation()) { + BigInteger biNewWindowSize = BigInteger.valueOf(remainUsage) + .multiply(BigInteger.valueOf(remainWindowSize)) + .add(BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(this.windowSize)) + .multiply(BigInteger.valueOf(WINDOW_SIZE_PRECISION))); + newWindowSize = divideCeilExact(biNewWindowSize, BigInteger.valueOf(newUsage)); + } else { + newWindowSize = divideCeil( + remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, + newUsage); + } newWindowSize = min(newWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); accountCapsule.setNewWindowSizeV2(resourceCode, newWindowSize); @@ -191,10 +240,18 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece remainReceiverWindowSizeV2 = remainReceiverWindowSizeV2 < 0 ? 0 : remainReceiverWindowSizeV2; // calculate new windowSize - long newOwnerWindowSize = - divideCeil( - ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, - newOwnerUsage); + long newOwnerWindowSize; + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(ownerUsage) + .multiply(BigInteger.valueOf(remainOwnerWindowSizeV2)) + .add(BigInteger.valueOf(transferUsage) + .multiply(BigInteger.valueOf(remainReceiverWindowSizeV2))); + newOwnerWindowSize = divideCeilExact(bi, BigInteger.valueOf(newOwnerUsage)); + } else { + newOwnerWindowSize = divideCeil( + ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, + newOwnerUsage); + } newOwnerWindowSize = min(newOwnerWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); owner.setNewWindowSizeV2(resourceCode, newOwnerWindowSize); @@ -204,6 +261,11 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece private long getNewWindowSize(long lastUsage, long lastWindowSize, long usage, long windowSize, long newUsage) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(lastUsage).multiply(BigInteger.valueOf(lastWindowSize)) + .add(BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize))); + return bi.divide(BigInteger.valueOf(newUsage)).longValueExact(); + } return (lastUsage * lastWindowSize + usage * windowSize) / newUsage; } @@ -211,11 +273,29 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenCalculation()) { + return BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)).longValueExact(); + } return usage * windowSize / precision; } private long getUsage(long oldUsage, long oldWindowSize, long newUsage, long newWindowSize) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(oldUsage).multiply(BigInteger.valueOf(oldWindowSize)) + .add(BigInteger.valueOf(newUsage).multiply(BigInteger.valueOf(newWindowSize))); + return bi.divide(BigInteger.valueOf(precision)).longValueExact(); + } return (oldUsage * oldWindowSize + newUsage * newWindowSize) / precision; } @@ -262,4 +342,38 @@ protected boolean consumeFeeForNewAccount(AccountCapsule accountCapsule, long fe protected boolean disableJavaLangMath() { return dynamicPropertiesStore.disableJavaLangMath(); } + + protected boolean hardenCalculation() { + return dynamicPropertiesStore.allowHardenResourceCalculation(); + } + + protected long calculateGlobalLimitV1(long frozeBalance, + long totalLimit, long totalWeight) { + long weight = frozeBalance / TRX_PRECISION; + return BigInteger.valueOf(weight) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(totalWeight)) + .longValueExact(); + } + + /** + * Hardened replacement of legacy V2 formula + * {@code (long)(((double) frozeBalance / TRX_PRECISION) + * * ((double) totalLimit / totalWeight))}. + * + *

Preserves V2 semantics: equivalent to + * {@code (frozeBalance * totalLimit) / (TRX_PRECISION * totalWeight)} with + * a single integer truncation at the end. Critically, fractional weight + * (i.e. {@code frozeBalance < TRX_PRECISION}) is preserved through the + * multiplication and only truncated at the final divide, so small balances + * yield the same proportional result as the double-arithmetic path. + */ + protected long calculateGlobalLimitV2(long frozeBalance, + long totalLimit, long totalWeight) { + return BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(TRX_PRECISION) + .multiply(BigInteger.valueOf(totalWeight))) + .longValueExact(); + } } diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index e0adb0d444a..12dfca5e59a 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] ALLOW_HARDEN_RESOURCE_CALCULATION = + "ALLOW_HARDEN_RESOURCE_CALCULATION".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2996,21 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public long getAllowHardenResourceCalculation() { + return Optional.ofNullable(getUnchecked(ALLOW_HARDEN_RESOURCE_CALCULATION)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowHardenResourceCalculation(long value) { + this.put(ALLOW_HARDEN_RESOURCE_CALCULATION, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowHardenResourceCalculation() { + return getAllowHardenResourceCalculation() == 1L; + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 1a7f0c058a4..94c1e50284e 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -63,6 +63,8 @@ public class VMConfig { private static boolean ALLOW_TVM_OSAKA = false; + private static boolean ALLOW_HARDEN_RESOURCE_CALCULATION = false; + private VMConfig() { } @@ -178,6 +180,10 @@ public static void initAllowTvmOsaka(long allow) { ALLOW_TVM_OSAKA = allow == 1; } + public static void initAllowHardenResourceCalculation(long allow) { + ALLOW_HARDEN_RESOURCE_CALCULATION = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -281,4 +287,8 @@ public static boolean allowTvmSelfdestructRestriction() { public static boolean allowTvmOsaka() { return ALLOW_TVM_OSAKA; } + + public static boolean allowHardenResourceCalculation() { + return ALLOW_HARDEN_RESOURCE_CALCULATION; + } } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index efbceda1b16..2c35fbd6447 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1481,6 +1481,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowHardenResourceCalculation") + .setValue(dbManager.getDynamicPropertiesStore().getAllowHardenResourceCalculation()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index 1bec0c2bda3..54e0c6fa362 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -396,6 +396,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue()); break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + manager.getDynamicPropertiesStore() + .saveAllowHardenResourceCalculation(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index c26da5a7bf7..2a9de1ea3b1 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -343,6 +343,8 @@ public void validateCheck() { testAllowTvmSelfdestructRestrictionProposal(); + testAllowHardenResourceCalculationProposal(); + forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats); forkUtils.reset(); @@ -569,6 +571,48 @@ private void testAllowTvmSelfdestructRestrictionProposal() { e3.getMessage()); } + private void testAllowHardenResourceCalculationProposal() { + byte[] stats = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]", + e1.getMessage()); + + long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() + .getMaintenanceTimeInterval(); + + long hardForkTime = + ((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime + 1); + + stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + + // Should fail because the proposal value is invalid + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1", + e2.getMessage()); + + dynamicPropertiesStore.saveAllowHardenResourceCalculation(1); + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, no need to propose again", + e3.getMessage()); + } + private void testAllowMarketTransaction() { ThrowingRunnable off = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.ALLOW_MARKET_TRANSACTION.getCode(), 0); diff --git a/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java new file mode 100644 index 00000000000..1df362fff42 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java @@ -0,0 +1,346 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class CalculateGlobalLimitHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + } + + private EnergyProcessor energyProcessor; + private BandwidthProcessor bandwidthProcessor; + private AccountCapsule ownerCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + energyProcessor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + bandwidthProcessor = new BandwidthProcessor(dbManager.getChainBaseManager()); + } + + @After + public void tearDown() { + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + } + + @Test + public void testGlobalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(1L); + ownerCapsule.setFrozenForEnergy(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.calculateGlobalEnergyLimit(ownerCapsule)); + } + + @Test + public void testGlobalEnergyLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitV2CorrectVsDoublePrecisionLoss() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; // ~9.8e15 + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalEnergyWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalNetLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + ownerCapsule.setFrozenForBandwidth(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(1L); + ownerCapsule.setFrozenForBandwidth(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule)); + } + + + @Test + public void testGlobalNetLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2ExactPrecision() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalNetLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalNetWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalEnergyLimitV2BelowTrxPrecisionMatchesDouble() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION (1_000_000) + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(12L, resultNew); + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2BelowTrxPrecisionMatchesDouble() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertTrue("non-zero proportional result expected", resultNew > 0); + } + + @Test + public void testGlobalEnergyLimitV1NonIntegerRatioParity() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; // not an exact divisor of totalEnergyLimit + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testV1FlooredWeightVsV2FractionalWeight() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 1_500_000L; // 1.5 x TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + // V1 path + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + ownerCapsule.setFrozenForEnergy(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + long v1New = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + // Legacy V1 expectation: floor(1.5) * 25.0 = 1 * 25 = 25 + Assert.assertEquals(25L, v1New); + + // V2 path with the same balance keeps the fractional weight + long v2New = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + // Legacy V2 expectation: 1.5 * 25.0 = 37.5 -> 37 + Assert.assertEquals(37L, v2New); + + // And both must match their respective legacy doubles + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long v1Old = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + long v2Old = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(v1Old, v1New); + Assert.assertEquals(v2Old, v2New); + } + + @Test + public void testGlobalNetLimitV1UsesTotalNetWeightNotLimit() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; // distinct from totalNetLimit + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + ownerCapsule.setFrozenForBandwidth(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + @Test + public void testGlobalNetLimitV2UsesTotalNetWeightNotLimit() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + + @Test + public void testUpdateAdaptiveTotalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(20_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(10_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultOld = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultNew = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUpdateAdaptiveTotalEnergyLimitOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit( + 10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } + + @Test + public void testUpdateAdaptiveLimitMultiplierOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(1_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(Long.MAX_VALUE / 100); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } +} diff --git a/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java new file mode 100644 index 00000000000..ee096abd382 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java @@ -0,0 +1,281 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.contract.Common; +import org.tron.protos.contract.Common.ResourceCode; + +@Slf4j +public class ResourceProcessorHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + private static final String RECEIVER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + RECEIVER_ADDRESS = + Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc"; + } + + private EnergyProcessor processor; + private AccountCapsule ownerCapsule; + private AccountCapsule receiverCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + receiverCapsule = new AccountCapsule( + ByteString.copyFromUtf8("receiver"), + ByteString.copyFrom(ByteArray.fromHexString(RECEIVER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(10000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(10_000_000L); + + processor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test + public void testIncreaseNormalValuesConsistent() { + long lastUsage = 1000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; // 24h in slots + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long lastUsage = 70_000_000L; + long usage = 2345L; + long lastTime = 9999L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increase(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + processor.increase(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testIncreaseAcceptsIntermediateOverflowWhenResultFits() { + long lastUsage = Long.MAX_VALUE / 100; // ~9.2e16 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be a valid long", result >= 0); + } + + @Test + public void testIncreaseWithAccountCapsuleConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + + long lastUsage = 5_000_000L; + long usage = 1_000L; + long lastTime = 9990L; + long now = 9995L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowOld = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowNew = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 1000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2ConsistentWithHardening() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 5_000_000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testIncreaseV2OverflowDetected() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17, above threshold + long usage = 1000L; + long lastTime = 9999L; + long now = 10000L; + + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now)); + } + + @Test + public void testLargeButSafeValuesWithHardening() { + long lastUsage = 300_000_000_000L; // 300 billion + long usage = 100L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be positive", result > 0); + } + + private void setupForUnDelegate(long now) { + setupForUnDelegateWithUsage(now, 5_000_000L, 3_000_000L); + } + + private void setupForUnDelegateWithUsage(long now, long ownerUsage, long receiverUsage) { + ownerCapsule.setLatestConsumeTimeForEnergy(now); + ownerCapsule.setEnergyUsage(ownerUsage); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + receiverCapsule.setLatestConsumeTimeForEnergy(now - 100); + receiverCapsule.setEnergyUsage(receiverUsage); + receiverCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + receiverCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + } +} diff --git a/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java new file mode 100644 index 00000000000..6b15409edd6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java @@ -0,0 +1,280 @@ +package org.tron.core.vm.repository; + +import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.VMConfig; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class RepositoryImplHardenTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + private RepositoryImpl repository; + private Method increaseMethod; + private Method getUsageMethod; + private Method usageToBalanceMethod; + + @Before + public void setUp() throws Exception { + repository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + + increaseMethod = RepositoryImpl.class.getDeclaredMethod( + "increase", long.class, long.class, long.class, long.class, long.class); + increaseMethod.setAccessible(true); + + getUsageMethod = RepositoryImpl.class.getDeclaredMethod( + "getUsage", long.class, long.class); + getUsageMethod.setAccessible(true); + + usageToBalanceMethod = RepositoryImpl.class.getDeclaredMethod( + "usageToBalance", long.class, long.class, long.class); + usageToBalanceMethod.setAccessible(true); + } + + @After + public void tearDown() { + VMConfig.initAllowHardenResourceCalculation(0); + } + + private long invokeIncrease(long lastUsage, long usage, long lastTime, + long now, long windowSize) throws Exception { + try { + return (long) increaseMethod.invoke( + repository, lastUsage, usage, lastTime, now, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeGetUsage(long usage, long windowSize) throws Exception { + try { + return (long) getUsageMethod.invoke(repository, usage, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeUsageToBalance(long usage, long totalWeight, long totalLimit) + throws Exception { + try { + return (long) usageToBalanceMethod.invoke( + repository, usage, totalWeight, totalLimit); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + @Test + public void testIncreaseNormalValuesParity() throws Exception { + long lastUsage = 1_000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGetUsageNormalValuesParity() throws Exception { + long usage = 100_000L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeGetUsage(usage, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeGetUsage(usage, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeIncrease(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() throws Exception { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testGetUsageCorrectAcrossOverflowBoundary() throws Exception { + long usage = Long.MAX_VALUE / 1000; // ~9.2e15 + long windowSize = 28800L; + + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + + VMConfig.initAllowHardenResourceCalculation(0); + long wrapped = invokeGetUsage(usage, windowSize); + Assert.assertNotEquals(expected, wrapped); + } + + @Test + public void testGetUsageLargeButSafeWithHardening() throws Exception { + long usage = 500_000_000_000L; // 5e11 + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + } + + + @Test + public void testUsageToBalanceParity() throws Exception { + long usage = 1_000_000L; + long totalWeight = 2_000_000_000L; + long totalLimit = 50_000_000_000L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeUsageToBalance(usage, totalWeight, totalLimit); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUsageToBalanceCorrectAcrossDoublePrecision() throws Exception { + long usage = 100_000_000L; // 1e8 + long totalWeight = 100_000_000_000L; // 1e11 -> usage * weight = 1e19, beyond 2^53 + long totalLimit = 50_000_000_000L; + + java.math.BigInteger expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(totalWeight)) + .multiply(java.math.BigInteger.valueOf(1_000_000L)) + .divide(java.math.BigInteger.valueOf(totalLimit)); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testUsageToBalanceOverflowDetectedWithHardening() { + long usage = 1_000_000_000L; + long totalWeight = 1_000_000_000_000L; + long totalLimit = 1L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeUsageToBalance(usage, totalWeight, totalLimit)); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedParityWithNonIntegerRatio() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = repository.calculateGlobalEnergyLimit(account); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = repository.calculateGlobalEnergyLimit(account); + + long expected = java.math.BigInteger.valueOf(10000L) + .multiply(java.math.BigInteger.valueOf(totalEnergyLimit)) + .divide(java.math.BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + Assert.assertEquals(expected, resultNew); + Assert.assertEquals(resultOld, resultNew); + + long buggy = 10000L * (totalEnergyLimit / totalEnergyWeight); + Assert.assertNotEquals(buggy, resultNew); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedOverflowDetected() { + long totalEnergyLimit = Long.MAX_VALUE / 2; + long totalEnergyWeight = 1L; + long frozeBalance = Long.MAX_VALUE / 4; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(1); + Assert.assertThrows(ArithmeticException.class, + () -> repository.calculateGlobalEnergyLimit(account)); + } +} From 6c56161c2881e14fa7a96a40dc1b1b3eb7f0dbb0 Mon Sep 17 00:00:00 2001 From: xxo1_shine Date: Thu, 7 May 2026 20:39:15 +0800 Subject: [PATCH 35/43] feat: fix TRX inv rate limit bug and add BLOCK inv rate limit (#6731) Co-authored-by: zeusoo001 Co-authored-by: Claude Sonnet 4.6 --- .../common/parameter/CommonParameter.java | 3 + .../org/tron/core/config/args/NodeConfig.java | 6 ++ common/src/main/resources/reference.conf | 2 + .../java/org/tron/core/config/args/Args.java | 1 + .../tron/core/net/P2pEventHandlerImpl.java | 43 +++++--- .../core/net/P2pEventHandlerImplTest.java | 99 ++++++++++++++++++- 6 files changed, 137 insertions(+), 17 deletions(-) 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 a73158a718a..e7957c917e2 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -119,6 +119,9 @@ public class CommonParameter { public int maxTps; // clearParam: 1000 @Getter @Setter + public int maxBlockInvPerSecond = 10; // default: 10 block inv hashes/s per peer + @Getter + @Setter public int minParticipationRate; @Getter public P2pConfig p2pConfig; 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 c3305e976de..f9cf05a917f 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 @@ -35,6 +35,7 @@ public class NodeConfig { private boolean openPrintLog = true; private boolean openTransactionSort = false; private int maxTps = 1000; + private int maxBlockInvPerSecond = 10; // Config key "isOpenFullTcpDisconnect" cannot auto-bind — read manually in fromConfig() @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) @@ -452,6 +453,11 @@ private void postProcess() { inactiveThreshold = 1; } + // maxBlockInvPerSecond: minimum 1 + if (maxBlockInvPerSecond < 1) { + maxBlockInvPerSecond = 1; + } + // maxFastForwardNum: clamp to [1, MAX_ACTIVE_WITNESS_NUM] if (maxFastForwardNum > MAX_ACTIVE_WITNESS_NUM) { maxFastForwardNum = MAX_ACTIVE_WITNESS_NUM; diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 11970a0a673..07f2114d061 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -227,6 +227,8 @@ node { # Threshold for broadcast transactions received from each peer per second, # transactions exceeding this are discarded maxTps = 1000 + # Max block inv hashes accepted per peer per second. Minimum: 1. + maxBlockInvPerSecond = 10 isOpenFullTcpDisconnect = false inactiveThreshold = 600 // seconds 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 f91c6a437ac..62786b23927 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 @@ -615,6 +615,7 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.minActiveConnections = nc.getMinActiveConnections(); PARAMETER.maxConnectionsWithSameIp = nc.getMaxConnectionsWithSameIp(); PARAMETER.maxTps = nc.getMaxTps(); + PARAMETER.maxBlockInvPerSecond = nc.getMaxBlockInvPerSecond(); PARAMETER.minParticipationRate = nc.getMinParticipationRate(); PARAMETER.nodeListenPort = nc.getListenPort(); PARAMETER.nodeEnableIpv6 = nc.isEnableIpv6(); diff --git a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java index 9cfa5058e8c..b9173b95cde 100644 --- a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java +++ b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java @@ -36,6 +36,7 @@ import org.tron.core.net.service.effective.EffectiveCheckService; import org.tron.core.net.service.handshake.HandshakeService; import org.tron.core.net.service.keepalive.KeepAliveService; +import org.tron.core.net.service.statistics.MessageStatistics; import org.tron.p2p.P2pEventHandler; import org.tron.p2p.connection.Channel; import org.tron.protos.Protocol; @@ -91,6 +92,7 @@ public class P2pEventHandlerImpl extends P2pEventHandler { private byte MESSAGE_MAX_TYPE = 127; private int maxCountIn10s = Args.getInstance().getMaxTps() * 10; + private int maxBlockInvIn10s = Args.getInstance().getMaxBlockInvPerSecond() * 10; public P2pEventHandlerImpl() { Set set = new HashSet<>(); @@ -149,19 +151,8 @@ private void processMessage(PeerConnection peer, byte[] data) { msg = TronMessageFactory.create(data); type = msg.getType(); - if (INVENTORY.equals(type)) { - InventoryMessage message = (InventoryMessage) msg; - Protocol.Inventory.InventoryType inventoryType = message.getInventoryType(); - int count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement - .getCount(10); - if (inventoryType.equals(Protocol.Inventory.InventoryType.TRX) && count > maxCountIn10s) { - logger.warn("Drop inventory from Peer {}, cur:{}, max:{}", - peer.getInetAddress(), count, maxCountIn10s); - if (Args.getInstance().isOpenPrintLog()) { - logger.warn("[overload]Drop tx list is: {}", ((InventoryMessage) msg).getHashList()); - } - return; - } + if (INVENTORY.equals(type) && !checkInvRateLimit(peer, (InventoryMessage) msg)) { + return; } peer.getPeerStatistics().messageStatistics.addTcpInMessage(msg); @@ -224,6 +215,32 @@ private void processMessage(PeerConnection peer, byte[] data) { } } + private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) { + InventoryType invType = msg.getInventoryType(); + int currentSize = msg.getInventory().getIdsCount(); + MessageStatistics stats = peer.getPeerStatistics().messageStatistics; + + if (invType == InventoryType.TRX) { + int count = stats.tronInTrxInventoryElement.getCount(10); + if (count + currentSize > maxCountIn10s) { + logger.warn("Drop TRX inv from {}, window:{}, cur:{}, max:{}", + peer.getInetAddress(), count, currentSize, maxCountIn10s); + if (Args.getInstance().isOpenPrintLog()) { + logger.warn("[overload] Drop tx list: {}", msg.getHashList()); + } + return false; + } + } else if (invType == InventoryType.BLOCK) { + int count = stats.tronInBlockInventoryElement.getCount(10); + if (count + currentSize > maxBlockInvIn10s) { + logger.warn("Drop BLOCK inv from {}, window:{}, cur:{}, max:{}", + peer.getInetAddress(), count, currentSize, maxBlockInvIn10s); + return false; + } + } + return true; + } + private void updateLastInteractiveTime(PeerConnection peer, TronMessage msg) { MessageTypes type = msg.getType(); diff --git a/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java index 03c79f495ee..2e79bbf5809 100644 --- a/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java +++ b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java @@ -34,6 +34,7 @@ public static void init() throws Exception { public void testProcessInventoryMessage() throws Exception { CommonParameter parameter = CommonParameter.getInstance(); parameter.setMaxTps(10); + parameter.setMaxBlockInvPerSecond(10); PeerStatistics peerStatistics = new PeerStatistics(); @@ -75,7 +76,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10); - Assert.assertEquals(110, count); + Assert.assertEquals(10, count); // 100 hashes dropped: 10+100=110 > maxCountIn10s(100) list.clear(); for (int i = 0; i < 100; i++) { @@ -88,7 +89,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10); - Assert.assertEquals(110, count); + Assert.assertEquals(10, count); // still dropped: window=10, 10+100=110 > 100 list.clear(); for (int i = 0; i < 200; i++) { @@ -101,7 +102,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10); - Assert.assertEquals(200, count); + Assert.assertEquals(0, count); // 200 hashes dropped: 0+200=200 > maxBlockInvIn10s(100) list.clear(); for (int i = 0; i < 100; i++) { @@ -114,10 +115,100 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10); - Assert.assertEquals(300, count); + Assert.assertEquals(100, count); // passes: window=0, 0+100=100, not > 100 } + @Test + public void testCheckInvRateLimitTrxBoundary() throws Exception { + // maxTps=10 → maxCountIn10s=100 + CommonParameter parameter = CommonParameter.getInstance(); + parameter.setMaxTps(10); + parameter.setMaxBlockInvPerSecond(10); + + PeerStatistics peerStatistics = new PeerStatistics(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getPeerStatistics()).thenReturn(peerStatistics); + + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + Method method = handler.getClass() + .getDeclaredMethod("processMessage", PeerConnection.class, byte[].class); + method.setAccessible(true); + + // Fill window to 91: send 91 TRX hashes → passes (0+91=91 ≤ 100) + List list91 = new ArrayList<>(); + for (int i = 0; i < 91; i++) { + list91.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msg91 = new InventoryMessage(list91, InventoryType.TRX); + method.invoke(handler, peer, msg91.getSendBytes()); + Assert.assertEquals(91, + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + + // Send 9 more TRX hashes → passes (91+9=100, not > 100) + List list9 = new ArrayList<>(); + for (int i = 0; i < 9; i++) { + list9.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msg9 = new InventoryMessage(list9, InventoryType.TRX); + method.invoke(handler, peer, msg9.getSendBytes()); + Assert.assertEquals(100, + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + + // Send 1 more TRX hash → DROPPED (100+1=101 > 100) + List list1 = new ArrayList<>(); + list1.add(new Sha256Hash(0, new byte[32])); + InventoryMessage msg1 = new InventoryMessage(list1, InventoryType.TRX); + method.invoke(handler, peer, msg1.getSendBytes()); + Assert.assertEquals(100, // count unchanged: message was dropped + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + } + + @Test + public void testCheckInvRateLimitBlockBoundary() throws Exception { + // maxBlockInvPerSecond=10 → maxBlockInvIn10s=100 + CommonParameter parameter = CommonParameter.getInstance(); + parameter.setMaxTps(1000); + parameter.setMaxBlockInvPerSecond(10); + + PeerStatistics peerStatistics = new PeerStatistics(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getPeerStatistics()).thenReturn(peerStatistics); + + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + Method method = handler.getClass() + .getDeclaredMethod("processMessage", PeerConnection.class, byte[].class); + method.setAccessible(true); + + // Send 101 BLOCK hashes → DROPPED (0+101=101 > 100) + List list101 = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + list101.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msgBlock101 = new InventoryMessage(list101, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock101.getSendBytes()); + Assert.assertEquals(0, + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + + // Send 100 BLOCK hashes → passes (0+100=100, not > 100) + List list100 = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + list100.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msgBlock100 = new InventoryMessage(list100, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock100.getSendBytes()); + Assert.assertEquals(100, + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + + // Send 1 more BLOCK hash → DROPPED (100+1=101 > 100) + List list1 = new ArrayList<>(); + list1.add(new Sha256Hash(0, new byte[32])); + InventoryMessage msgBlock1 = new InventoryMessage(list1, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock1.getSendBytes()); + Assert.assertEquals(100, // count unchanged: message was dropped + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + } + @Test public void testUpdateLastInteractiveTime() throws Exception { PeerConnection peer = new PeerConnection(); From bd3b5ef7ce86e83113bce7bc82872e8d80663e5e Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Thu, 7 May 2026 20:55:52 +0800 Subject: [PATCH 36/43] feat(db,lite): add --exclude-historical-balance for lite snapshot (#6690) * feat(toolkit): add --exclude-historical-balance for lite snapshot split Default behavior is unchanged: balance-trace and account-trace stay in the lite snapshot as before, so default operators (historyBalanceLookup=off) see no difference. Opt-in via `--exclude-historical-balance=true` on `split -t snapshot` excludes the two trace stores from the snapshot for size-conscious operators. A loud warning is printed at split time noting that this loss is permanent for nodes that had historyBalanceLookup=true (merge cannot restore the feature) and that operators who need historical balance lookup on the resulting lite node must NOT enable this flag. `split -t history` and `merge` ignore the flag and continue using the legacy 5-db archive set, so merge logic stays untouched. Includes: - DbLite: new CLI option, helper method, runtime warning. - README: parameter documentation and worked example. - DbLiteTest: 3-arg testTools overload and packaging-contract assertion. - DbLiteExcludeHistoricalBalanceRocksDbTest: opt-in path coverage. close #6597 * docs(toolkit): clarify lite snapshot trace exclusion --- plugins/README.md | 6 +- .../java/common/org/tron/plugins/DbLite.java | 71 ++++++++++++++++++- .../java/org/tron/plugins/DbLiteTest.java | 34 +++++++-- ...teExcludeHistoricalBalanceRocksDbTest.java | 13 ++++ 4 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java diff --git a/plugins/README.md b/plugins/README.md index b6540beef1a..f14e070c01a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -75,16 +75,20 @@ DB lite provides lite database, parameters are compatible with previous `LiteFul - `-fn | --fn-data-path`: The database path to be split or merged. - `-ds | --dataset-path`: When operation is `split`,`dataset-path` is the path that store the `snapshot` or `history`, when operation is `split`, `dataset-path` is the `history` data path. +- `--exclude-historical-balance`: Only used with `operate=split -t snapshot`, default: false. When set to true, `balance-trace` and `account-trace` are excluded from the lite snapshot. The flag has functional impact only when the source full node ran with `historyBalanceLookup=true` (off by default; most operators are unaffected). **WARNING:** for nodes that had `historyBalanceLookup=true`, this loss is permanent — a lite node booted from such a snapshot cannot safely serve historical balance lookups (`getBlockBalance` may fail, and `getAccountBalance` may return `balance=0` when `account-trace` data is missing), and running `merge` afterwards will NOT restore the feature. If you need historical balance lookup on the resulting lite node, do **not** enable this flag. `split -t history` and `merge` ignore this flag. - `-h | --help`: Provide the help info. ### Examples: ```shell script # full command - java -jar Toolkit.jar db lite [-h] -ds= -fn= [-o=] [-t=] + java -jar Toolkit.jar db lite [-h] -ds= -fn= [-o=] [-t=] [--exclude-historical-balance] # examples #split and get a snapshot dataset java -jar Toolkit.jar db lite -o split -t snapshot --fn-data-path output-directory/database --dataset-path /tmp + #split and get a snapshot dataset without balance-trace / account-trace (smaller snapshot; + #historical balance lookup cannot be safely served on the resulting lite node) + java -jar Toolkit.jar db lite -o split -t snapshot --fn-data-path output-directory/database --dataset-path /tmp --exclude-historical-balance #split and get a history dataset java -jar Toolkit.jar db lite -o split -t history --fn-data-path output-directory/database --dataset-path /tmp #merge history dataset and snapshot dataset diff --git a/plugins/src/main/java/common/org/tron/plugins/DbLite.java b/plugins/src/main/java/common/org/tron/plugins/DbLite.java index 3f8a6cb58c8..1a7e4e270f7 100644 --- a/plugins/src/main/java/common/org/tron/plugins/DbLite.java +++ b/plugins/src/main/java/common/org/tron/plugins/DbLite.java @@ -20,6 +20,7 @@ import java.util.concurrent.Callable; import java.util.stream.Collectors; import java.util.stream.LongStream; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import me.tongfei.progressbar.ProgressBar; import org.rocksdb.RocksDBException; @@ -57,6 +58,8 @@ public class DbLite implements Callable { private static final String TRANSACTION_HISTORY_DB_NAME = "transactionHistoryStore"; private static final String PROPERTIES_DB_NAME = "properties"; private static final String TRANS_CACHE_DB_NAME = "trans-cache"; + private static final String BALANCE_TRACE_DB_NAME = "balance-trace"; + private static final String ACCOUNT_TRACE_DB_NAME = "account-trace"; private static final List archiveDbs = Arrays.asList( BLOCK_DB_NAME, @@ -65,6 +68,10 @@ public class DbLite implements Callable { TRANSACTION_RET_DB_NAME, TRANSACTION_HISTORY_DB_NAME); + private static final List traceDbs = Arrays.asList( + BALANCE_TRACE_DB_NAME, + ACCOUNT_TRACE_DB_NAME); + enum Operate { split, merge } enum Type { snapshot, history } @@ -105,8 +112,26 @@ enum Type { snapshot, history } private String datasetPath; @CommandLine.Option( - names = {"--help", "-h"}, + names = {"--exclude-historical-balance"}, + defaultValue = "false", + description = "only used with `operate=split -t snapshot`: when true, balance-trace " + + "and account-trace are excluded from the lite snapshot. " + + "Default: ${DEFAULT-VALUE} (legacy behavior; trace stores stay in the snapshot). " + + "This flag only has a functional impact when the source full node ran with " + + "`historyBalanceLookup=true` (off by default; most operators are unaffected). " + + "WARNING: when historyBalanceLookup was enabled, this loss is permanent: a lite " + + "node booted from such a snapshot cannot safely serve historical balance lookups " + + "(getBlockBalance may fail, and getAccountBalance may return balance=0 when " + + "account-trace data is missing). Running merge afterwards will NOT restore the " + + "feature. If you need to keep historyBalanceLookup working on the resulting " + + "lite node, do NOT enable this flag. `split -t history` and `merge` ignore " + + "this flag.", order = 5) + private boolean excludeHistoricalBalance; + + @CommandLine.Option( + names = {"--help", "-h"}, + order = 6) private boolean help; @@ -120,6 +145,7 @@ public Integer call() { switch (this.operate) { case split: if (Type.snapshot == this.type) { + warnIfExcludingHistoricalBalance(); generateSnapshot(fnDataPath, datasetPath); } else if (Type.history == type) { generateHistory(fnDataPath, datasetPath); @@ -253,12 +279,52 @@ public void completeHistoryData(String historyDir, String liteDir) { spec.commandLine().getOut().format("Merge history finished, take %d s.", during).println(); } + /** + * Compute the directories to exclude from the lite snapshot. + *

+ * Default ({@code --exclude-historical-balance=false}): the legacy archive set + * (5 dbs); {@link #BALANCE_TRACE_DB_NAME} / {@link #ACCOUNT_TRACE_DB_NAME} + * stay with the snapshot as state-style stores. + *

+ * Opt-in ({@code --exclude-historical-balance=true}): the trace stores are + * additionally excluded, producing a smaller lite snapshot at the cost of + * dropping historical balance lookup support on the resulting lite node. + * Only {@code split -t snapshot} consults this. {@code split -t history} + * and {@code merge} always use the legacy archive set. + */ + private List snapshotExclusion() { + if (!excludeHistoricalBalance) { + return archiveDbs; + } + return Stream.concat(archiveDbs.stream(), traceDbs.stream()) + .collect(Collectors.toList()); + } + + private void warnIfExcludingHistoricalBalance() { + if (!excludeHistoricalBalance) { + return; + } + String msg = "WARNING: --exclude-historical-balance is enabled. balance-trace / account-trace " + + "will be excluded from the lite snapshot. This only matters when the source full " + + "node ran with historyBalanceLookup=true (off by default; most operators are " + + "unaffected). When that switch was enabled, this loss is permanent: lite nodes " + + "booted from this snapshot cannot safely serve historical balance lookups " + + "(getBlockBalance may fail, and getAccountBalance may return balance=0 when " + + "account-trace data is missing). Running merge afterwards will NOT restore the " + + "feature. If you need to keep historyBalanceLookup working on the resulting " + + "lite node, do NOT use this flag."; + logger.warn(msg); + spec.commandLine().getErr().println(spec.commandLine().getColorScheme() + .errorText(msg)); + } + private List getSnapshotDbs(String sourceDir) { List snapshotDbs = Lists.newArrayList(); File basePath = new File(sourceDir); + List excluded = snapshotExclusion(); Arrays.stream(Objects.requireNonNull(basePath.listFiles())) .filter(File::isDirectory) - .filter(dir -> !archiveDbs.contains(dir.getName())) + .filter(dir -> !excluded.contains(dir.getName())) .forEach(dir -> snapshotDbs.add(dir.getName())); return snapshotDbs; } @@ -723,4 +789,3 @@ public long getSnapshotMaxNum() { } - diff --git a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java index 07bddc461a0..f7cb7b7f74f 100644 --- a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java @@ -1,11 +1,14 @@ package org.tron.plugins; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.tron.common.utils.PublicMethod.getRandomPrivateKey; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import lombok.extern.slf4j.Slf4j; import org.junit.After; @@ -68,7 +71,7 @@ public void shutdown() throws InterruptedException { context.close(); } - public void init(String dbType) throws IOException { + public void init(String dbType, boolean historyBalanceLookup) throws IOException { dbPath = folder.newFolder().toString(); Args.setParam(new String[] { "-d", dbPath, "-w", "--p2p-disable", "true", "--storage-db-engine", dbType}, @@ -77,6 +80,7 @@ public void init(String dbType) throws IOException { Args.getInstance().setAllowAccountStateRoot(1); Args.getInstance().setRpcPort(PublicMethod.chooseRandomPort()); Args.getInstance().setRpcEnable(true); + Args.getInstance().setHistoryBalanceLookup(historyBalanceLookup); databaseDir = Args.getInstance().getStorage().getDbDirectory(); // init dbBackupConfig to avoid NPE Args.getInstance().dbBackupConfig = DbBackupConfig.getInstance(); @@ -89,10 +93,20 @@ public void clear() { public void testTools(String dbType, int checkpointVersion) throws InterruptedException, IOException { - logger.info("dbType {}, checkpointVersion {}", dbType, checkpointVersion); - init(dbType); - final String[] argsForSnapshot = - new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", + testTools(dbType, checkpointVersion, false); + } + + public void testTools(String dbType, int checkpointVersion, boolean excludeHistoricalBalance) + throws InterruptedException, IOException { + logger.info("dbType {}, checkpointVersion {}, excludeHistoricalBalance {}", + dbType, checkpointVersion, excludeHistoricalBalance); + boolean historyBalanceLookup = excludeHistoricalBalance; + init(dbType, historyBalanceLookup); + final String[] argsForSnapshot = excludeHistoricalBalance + ? new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", + dbPath + File.separator + databaseDir, "--dataset-path", + dbPath, "--exclude-historical-balance"} + : new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", dbPath + File.separator + databaseDir, "--dataset-path", dbPath}; final String[] argsForHistory = @@ -114,6 +128,16 @@ public void testTools(String dbType, int checkpointVersion) FileUtil.deleteDir(Paths.get(dbPath, databaseDir, "trans-cache").toFile()); // generate snapshot cli.execute(argsForSnapshot); + Path snapshotDir = Paths.get(dbPath, "snapshot"); + if (excludeHistoricalBalance) { + // when --exclude-historical-balance=true, the lite snapshot must not ship + // balance-trace / account-trace + assertFalse(snapshotDir.resolve("balance-trace").toFile().exists()); + assertFalse(snapshotDir.resolve("account-trace").toFile().exists()); + } else { + assertTrue(snapshotDir.resolve("balance-trace").toFile().exists()); + assertTrue(snapshotDir.resolve("account-trace").toFile().exists()); + } // start fullNode startApp(); // produce transactions diff --git a/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java new file mode 100644 index 00000000000..766fe6d0924 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java @@ -0,0 +1,13 @@ +package org.tron.plugins.rocksdb; + +import java.io.IOException; +import org.junit.Test; +import org.tron.plugins.DbLiteTest; + +public class DbLiteExcludeHistoricalBalanceRocksDbTest extends DbLiteTest { + + @Test + public void testToolsWithExcludeHistoricalBalance() throws InterruptedException, IOException { + testTools("ROCKSDB", 1, true); + } +} From d71e1e43ef758a5823e54538359d3ba73c86dcf6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 8 May 2026 10:19:39 +0800 Subject: [PATCH 37/43] feat(api): optimize and harden HTTP/JSON-RPC API layer (#6693) --- .../src/main/java/org/tron/core/Wallet.java | 41 +---- .../org/tron/core/services/RpcApiService.java | 2 +- .../services/http/RateLimiterServlet.java | 94 ++++++----- .../org/tron/core/services/http/Util.java | 6 +- .../core/services/jsonrpc/JsonRpcApiUtil.java | 36 +++++ .../services/jsonrpc/TronJsonRpcImpl.java | 22 +-- .../services/http/RateLimiterServletTest.java | 93 +++++++++++ .../tron/core/services/http/UtilMockTest.java | 148 +++++++++++++++++- .../services/jsonrpc/JsonRpcApiUtilTest.java | 78 +++++++++ 9 files changed, 425 insertions(+), 95 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 2c35fbd6447..ba1c42f3d08 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -241,7 +241,6 @@ import org.tron.protos.contract.BalanceContract.BlockBalanceTrace; import org.tron.protos.contract.BalanceContract.TransferContract; import org.tron.protos.contract.Common; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; import org.tron.protos.contract.ShieldContract.OutputPoint; import org.tron.protos.contract.ShieldContract.OutputPointInfo; @@ -569,41 +568,41 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(true).setCode(response_code.SUCCESS).build(); } } catch (ValidateSignatureException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.SIGERROR) .setMessage(ByteString.copyFromUtf8("Validate signature error: " + e.getMessage())) .build(); } catch (ContractValidateException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) .setMessage(ByteString.copyFromUtf8(CONTRACT_VALIDATE_ERROR + e.getMessage())) .build(); } catch (ContractExeException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) .setMessage(ByteString.copyFromUtf8("Contract execute error : " + e.getMessage())) .build(); } catch (AccountResourceInsufficientException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.BANDWITH_ERROR) .setMessage(ByteString.copyFromUtf8("Account resource insufficient error.")) .build(); } catch (DupTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8("Dup transaction.")) .build(); } catch (TaposException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TAPOS_ERROR) .setMessage(ByteString.copyFromUtf8("Tapos check error.")) .build(); } catch (TooBigTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); } catch (TransactionExpirationException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); @@ -2179,23 +2178,6 @@ public IncrementalMerkleVoucherInfo getMerkleTreeVoucherInfo(OutputPointInfo req return result.build(); } - public IncrementalMerkleTree getMerkleTreeOfBlock(long blockNum) throws ZksnarkException { - checkAllowShieldedTransactionApi(); - if (blockNum < 0) { - return null; - } - - try { - if (chainBaseManager.getMerkleTreeIndexStore().has(ByteArray.fromLong(blockNum))) { - return IncrementalMerkleTree - .parseFrom(chainBaseManager.getMerkleTreeIndexStore().get(blockNum)); - } - } catch (Exception ex) { - logger.error("GetMerkleTreeOfBlock failed, blockNum:{}", blockNum, ex); - } - - return null; - } public long getShieldedTransactionFee() { return chainBaseManager.getDynamicPropertiesStore().getShieldedTransactionFee(); @@ -2930,13 +2912,6 @@ public MarketOrderList getMarketOrderListByPair(byte[] sellTokenId, byte[] buyTo return builder.build(); } - public Transaction deployContract(TransactionCapsule trxCap) { - - // do nothing, so can add some useful function later - // trxCap contract para cacheUnpackValue has value - - return trxCap.getInstance(); - } public Transaction triggerContract(TriggerSmartContract triggerSmartContract, diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 63e7ba03fc7..bc50b79a36f 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -418,7 +418,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.error("Solidity NonUniqueObjectException: {}", e.getMessage()); + logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..80e62f38616 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,7 +3,11 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,56 +35,66 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + static final Map> ALLOWED_ADAPTERS; + static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + List> adapters = Arrays.asList( + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class); + Map> m = new HashMap<>(); + for (Class c : adapters) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @PostConstruct private void addRateContainer() { - RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() - .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } + RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() + .getRateLimiterInitialization().getHttpMap().get(name); + + String cName; + String params; + if (item == null) { + cName = DEFAULT_ADAPTER_NAME; + params = QpsStrategy.DEFAULT_QPS_PARAM; + } else { + cName = item.getStrategy(); + params = item.getParams(); } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); - } + + try { + container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); + } + } + + static IRateLimiter buildAdapter(String cName, String params, String name) { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw rateLimiterInitError(cName, params, name, + new IllegalArgumentException("unknown rate limiter adapter; allowed=" + + ALLOWED_ADAPTERS.keySet())); + } + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); } } - private void throwTronError(String strategy, String params, String servlet, Exception e) { - throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + private static TronError rateLimiterInitError(String strategy, String params, String servlet, + Exception e) { + return new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", e, TronError.ErrCode.RATE_LIMITER_INIT); } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..a0e8c77d646 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -95,7 +95,7 @@ public static String printErrorMsg(Exception e) { public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = new JSONObject(); JSONArray jsonArray = new JSONArray(); blocks.stream().forEach(block -> jsonArray.add(printBlockToJSON(block, selfType))); jsonObject.put("block", jsonArray); @@ -110,8 +110,10 @@ public static String printBlock(Block block, boolean selfType) { public static JSONObject printBlockToJSON(Block block, boolean selfType) { BlockCapsule blockCapsule = new BlockCapsule(block); String blockID = ByteArray.toHexString(blockCapsule.getBlockId().getBytes()); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(block, selfType)); + JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); + jsonObject.put("block_header", + JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); 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 70673d2148a..104b72a66e8 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 @@ -4,6 +4,7 @@ import com.google.common.primitives.Longs; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; @@ -597,6 +598,41 @@ public static long parseBlockTag(String tag, Wallet wallet) throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); } + /** + * Max allowed length for a JSON-RPC block number hex/decimal input. + * API-level DoS guard: rejects pathological inputs before BigInteger parsing, + * whose cost grows quadratically with length. Covers hex (0x + 64 chars for + * uint256) and decimal (78 chars for uint256) representations with headroom. + */ + private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + + /** + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit, rejecting + * negative values, and rejecting values that overflow a signed 64-bit + * block number. + */ + public static long parseBlockNumber(String blockNum) + throws JsonRpcInvalidParamsException { + if (blockNum == null || blockNum.length() > MAX_BLOCK_NUM_HEX_LEN) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + BigInteger value; + try { + value = ByteArray.hexToBigInteger(blockNum); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + if (value.signum() < 0) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return value.longValueExact(); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + } + /** * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use 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 ea8f15cd088..8c68f73fbc6 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 @@ -11,6 +11,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.alibaba.fastjson.JSON; @@ -354,11 +355,7 @@ private void requireLatestBlockTag(String blockNumOrTag) if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); } - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + parseBlockNumber(blockNumOrTag); throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } @@ -368,7 +365,6 @@ private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsEx } private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { - long blockNum; if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { // Return the head block directly from blockStore, bypassing blockIndexStore @@ -377,12 +373,7 @@ private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidPara } return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); } - try { - blockNum = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - return wallet.getBlockByNum(blockNum); + return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag)); } private BlockResult getBlockResult(Block block, boolean fullTx) { @@ -969,12 +960,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValueExact(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java new file mode 100644 index 00000000000..4ae76f85dfb --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -0,0 +1,93 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies RateLimiterServlet's adapter resolution: strict whitelist + * (no Class.forName arbitrary class loading), fail-fast on unknown or + * empty names, and successful construction of every whitelisted adapter. + */ +public class RateLimiterServletTest { + + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; + + @Test + public void testWhitelistContents() { + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testUnknownAdapterThrowsTronError() { + // Fail-fast parity with the pre-whitelist Class.forName behavior: an unknown + // adapter name raises TronError from @PostConstruct so Spring startup aborts + // rather than silently masking a misconfigured node. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("UnknownAdapter", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + assertTrue(e.getMessage().contains("UnknownAdapter")); + assertTrue(e.getMessage().contains("TestServlet")); + } + + @Test + public void testDefaultAdapterNameBuildsDefaultBaseQqsAdapter() { + // When no config entry exists for a servlet, addRateContainer passes + // DEFAULT_ADAPTER_NAME to buildAdapter; verify it resolves to + // DefaultBaseQqsAdapter. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyAdapterNameThrowsTronError() { + // Fail-fast parity with original: a configured-but-empty strategy name is + // a configuration bug and must not be silently replaced by the default. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + } + + @Test + public void testBuildsEachWhitelistedAdapter() { + // Exercises the newInstance(String) constructor path for every whitelisted + // adapter so a signature/strategy-class break on any entry fails here + // instead of at node startup. + assertTrue(RateLimiterServlet.buildAdapter( + QpsRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof QpsRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + IPQPSRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof IPQPSRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + GlobalPreemptibleAdapter.class.getSimpleName(), "permit=1", "TestServlet") + instanceof GlobalPreemptibleAdapter); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 221c5a7a165..7c05e0e9cfe 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -1,11 +1,15 @@ package org.tron.core.services.http; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -44,7 +48,7 @@ public void testPrintTransactionFee() { public void testPrintBlockList() { BlockCapsule blockCapsule1 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - BlockCapsule blockCapsule2 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + BlockCapsule blockCapsule2 = new BlockCapsule(2, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); GrpcAPI.BlockList list = GrpcAPI.BlockList.newBuilder() .addBlock(blockCapsule1.getInstance()) @@ -52,6 +56,148 @@ public void testPrintBlockList() { .build(); String out = Util.printBlockList(list, true); Assert.assertNotNull(out); + + JSONObject json = JSONObject.parseObject(out); + Assert.assertTrue(json.containsKey("block")); + JSONArray blockArray = json.getJSONArray("block"); + Assert.assertEquals(2, blockArray.size()); + + // verify each block has correct structure + for (int i = 0; i < blockArray.size(); i++) { + JSONObject blockJson = blockArray.getJSONObject(i); + Assert.assertTrue(blockJson.containsKey("blockID")); + Assert.assertTrue(blockJson.containsKey("block_header")); + Assert.assertFalse(blockJson.getString("blockID").isEmpty()); + JSONObject blockHeader = blockJson.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONEmptyTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertFalse(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + + @Test + public void testPrintBlockToJSONWithTransactions() { + // Structural invariants must hold under either visible flag; the flag-driven + // encoding difference is covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. + for (boolean visible : new boolean[]{true, false}) { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + blockCapsule.addTransaction(getTransactionCapsuleExample()); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), visible); + + String msg = "visible=" + visible; + Assert.assertTrue(msg, json.containsKey("blockID")); + Assert.assertTrue(msg, json.containsKey("block_header")); + Assert.assertTrue(msg, json.containsKey("transactions")); + Assert.assertFalse(msg, json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(msg, blockHeader); + Assert.assertTrue(msg, blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(msg, 1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(msg, txJson.containsKey("txID")); + Assert.assertTrue(msg, txJson.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { + // Pins the optimized printBlockToJSON against the prior behavior: the + // visible flag must still thread through to JsonFormat so address-bearing + // fields switch encoding while byte-identity fields stay stable. + ByteString witnessAddress = ByteString.copyFrom( + ByteArray.fromHexString("41548794500882809695a8a687866e76d4271a1abc")); + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), witnessAddress); + + JSONObject visible = Util.printBlockToJSON(blockCapsule.getInstance(), true); + JSONObject hidden = Util.printBlockToJSON(blockCapsule.getInstance(), false); + + // blockID is derived from raw bytes; identical under either flag. + Assert.assertEquals(visible.getString("blockID"), hidden.getString("blockID")); + + // Overall block_header must differ because witness_address is re-encoded. + String headerVisible = visible.getJSONObject("block_header").toJSONString(); + String headerHidden = hidden.getJSONObject("block_header").toJSONString(); + Assert.assertNotEquals(headerVisible, headerHidden); + + // visible=true renders a mainnet address as Base58 starting with 'T'. + String witnessVisible = visible.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessVisible); + Assert.assertTrue("visible=true witness_address should be Base58 ('T...'), got: " + + witnessVisible, witnessVisible.startsWith("T")); + + // visible=false keeps witness_address in raw (non-Base58) form. + String witnessHidden = hidden.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessHidden); + Assert.assertNotEquals(witnessVisible, witnessHidden); + } + + @Test + public void testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl() { + // Legacy impl produced JSON via JsonFormat.printToString(block, selfType), + // which omits repeated fields when empty. New impl mirrors that with an + // explicit isEmpty() guard. Pin parity using JsonFormat output as ground + // truth so a future refactor can't quietly start emitting "transactions": [] + // (or stop emitting the key when non-empty). + BlockCapsule empty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + assertTransactionsKeyMatchesLegacy(empty.getInstance(), false); + + BlockCapsule nonEmpty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + nonEmpty.addTransaction(getTransactionCapsuleExample()); + assertTransactionsKeyMatchesLegacy(nonEmpty.getInstance(), true); + } + + private static void assertTransactionsKeyMatchesLegacy(Protocol.Block block, + boolean expectTransactionsKey) { + JSONObject legacy = JSONObject.parseObject(JsonFormat.printToString(block, true)); + Assert.assertEquals("legacy JsonFormat parity broken — proto behavior changed?", + expectTransactionsKey, legacy.containsKey("transactions")); + + JSONObject actual = Util.printBlockToJSON(block, true); + Assert.assertEquals("new impl diverged from legacy on 'transactions' key presence", + expectTransactionsKey, actual.containsKey("transactions")); + } + + @Test + public void testPrintBlockToJSONCoversAllProtoTopLevelFields() { + // Guards against proto field drift: the old impl delegated to JsonFormat on + // the whole Block message, so any new top-level Block field appeared + // automatically. The new impl hand-assembles the JSON, so a future proto + // field would be silently dropped. Reflect over Block's descriptor and + // assert every declared top-level field is handled. + Map protoFieldToJsonKey = new HashMap<>(); + protoFieldToJsonKey.put("block_header", "block_header"); + // "transactions" is present only when non-empty; parity verified in + // testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl. + protoFieldToJsonKey.put("transactions", "transactions"); + + for (Descriptors.FieldDescriptor f : Protocol.Block.getDescriptor().getFields()) { + Assert.assertTrue( + "Block proto field '" + f.getName() + "' is not handled by printBlockToJSON. " + + "If you added a new top-level field, extend printBlockToJSON and this test.", + protoFieldToJsonKey.containsKey(f.getName())); + } } @Test diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java new file mode 100644 index 00000000000..6aaeea2cc4e --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -0,0 +1,78 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public class JsonRpcApiUtilTest { + + @Test + public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1aL, JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(0L, JsonRpcApiUtil.parseBlockNumber("0x0")); + } + + @Test + public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { + assertEquals(12345L, JsonRpcApiUtil.parseBlockNumber("12345")); + } + + @Test + public void parseBlockNumberAcceptsMaxLongValue() throws JsonRpcInvalidParamsException { + assertEquals(Long.MAX_VALUE, + JsonRpcApiUtil.parseBlockNumber("0x7fffffffffffffff")); + } + + @Test + public void parseBlockNumberRejectsNegative() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("-1")); + assertEquals("invalid block number", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x-1")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOverflow() { + // 2^64 - 1: fits uint64 but overflows signed long + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xffffffffffffffff")); + assertEquals("invalid block number", e1.getMessage()); + // 2^63: just past Long.MAX_VALUE + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x8000000000000000")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOversized() { + // 101 chars exceeds the 100-char limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(null)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xGG")); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsEmpty() { + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("")); + } +} From 16190df7b5eb7fcedafa6560a6cac6f013166f63 Mon Sep 17 00:00:00 2001 From: barbatos2011 <162298485+barbatos2011@users.noreply.github.com> Date: Fri, 8 May 2026 10:20:04 +0800 Subject: [PATCH 38/43] fix(plugins): exclude transitive leveldbjni-all from :crypto (#6738) --- .github/workflows/pr-build.yml | 36 ++++++++++++++++++++++++++++++++++ plugins/build.gradle | 25 ++++++++++++++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 249bcaf28f1..dd005f98b74 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -54,6 +54,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + build-ubuntu: name: Build ubuntu24 (JDK 17 / aarch64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'ubuntu' }} @@ -84,6 +93,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + docker-build-rockylinux: name: Build rockylinux (JDK 8 / x86_64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'rockylinux' }} @@ -127,6 +145,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon @@ -172,6 +199,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon --no-build-cache + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache diff --git a/plugins/build.gradle b/plugins/build.gradle index fc9ef5e00d6..09a13a19b1b 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,6 +39,16 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' + // x86 declares io.github.tronprotocol:leveldbjni-all:1.18.2 below; + // :crypto -> :common -> :platform also transitively pulls + // org.fusesource.leveldbjni:leveldbjni-all:1.8. Both jars carry + // org/iq80/leveldb/Options.class and the fat jar's last-write-wins + // merge can leave the 1.8 copy, which lacks Options.maxBatchSize(int) + // and breaks `db archive` at runtime. Drop the 1.8 transitive on x86 + // only; ARM64 has a single copy via :platform direct and no conflict. + if (!rootProject.archInfo.isArm64) { + exclude group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all' + } } implementation group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' @@ -49,9 +59,12 @@ dependencies { implementation project(":platform") } else { implementation project(":platform"), { + // Only leveldbjni-all is excluded; the io.github.tronprotocol + // 1.18.2 fork below is the version we want on x86. zksnark-java-sdk + // and commons-io are intentionally kept (the plugins test runtime + // needs both via :crypto -> :common -> :platform and the duplicate + // resolution dedups to one copy). exclude(group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all') - exclude(group: 'io.github.tronprotocol', module: 'zksnark-java-sdk') - exclude(group: 'commons-io', module: 'commons-io') } implementation 'io.github.tronprotocol:leveldbjni-all:1.18.2' implementation 'io.github.tronprotocol:leveldb:1.18.2' @@ -130,7 +143,13 @@ def binaryRelease(taskName, jarName, mainClass) { from(sourceSets.main.output) { include "/**" } - dependsOn (project(':protocol').jar, project(':platform').jar) // explicit_dependency + // Fat jar zips up runtimeClasspath, which includes the jar outputs of + // every project dependency. Declare them all explicitly so Gradle does + // not warn about implicit_dependency and disable execution optimizations + // (and so partial / parallel builds cannot run binaryRelease before the + // dependency jars exist). + dependsOn (project(':protocol').jar, project(':platform').jar, + project(':crypto').jar, project(':common').jar) // explicit_dependency from { configurations.runtimeClasspath.collect { // https://docs.gradle.org/current/userguide/upgrading_version_6.html#changes_6.3 it.isDirectory() ? it : zipTree(it) From 03bd4af8887b8aa351d4f9ff5f2b8be6fcaf9f72 Mon Sep 17 00:00:00 2001 From: "wayne.zhang" <43207186+waynercheung@users.noreply.github.com> Date: Fri, 8 May 2026 11:04:14 +0800 Subject: [PATCH 39/43] fix(jsonrpc): correct TransactionResult.nonce per JSON-RPC spec (#6709) Per ethereum/execution-apis, TransactionInfo.nonce is `uint` (QUANTITY) and must match `^0x(0|[1-9a-f][0-9a-f]*)$`. java-tron emitted the field as `0x0000000000000000` via `ByteArray.toJsonHex(new byte[8])`, which violates the pattern. Both `TransactionResult` constructors now emit `"0x0"`. Block.nonce is intentionally left at `0x0000000000000000` because the Block schema defines it as `bytes8`, so that value is already compliant and shortening it would break conformance. Closes #6547 --- .../services/jsonrpc/types/TransactionResult.java | 4 ++-- .../services/jsonrpc/TransactionResultTest.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java index 57650355d46..4f11c1a5908 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java @@ -98,7 +98,7 @@ public TransactionResult(BlockCapsule blockCapsule, int index, Protocol.Transact TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = ByteArray.toJsonHex(blockCapsule.getBlockId().getBytes()); blockNumber = ByteArray.toJsonHex(blockCapsule.getNum()); transactionIndex = ByteArray.toJsonHex(index); @@ -133,7 +133,7 @@ public TransactionResult(Transaction tx, Wallet wallet) { TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = "0x"; blockNumber = "0x"; transactionIndex = "0x"; diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java index 4e1af06199c..19c2bb6c4d3 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java @@ -23,10 +23,18 @@ public class TransactionResultTest extends BaseTest { private static final String OWNER_ADDRESS = "41548794500882809695a8a687866e76d4271a1abc"; private static final String CONTRACT_ADDRESS = "A0B4750E2CD76E19DCA331BF5D089B71C3C2798548"; + // QUANTITY pattern from ethereum/execution-apis base-types schema (uint). + private static final String QUANTITY_PATTERN = "^0x(0|[1-9a-f][0-9a-f]*)$"; + static { Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); } + private static void assertQuantity(String value) { + Assert.assertNotNull(value); + Assert.assertTrue("not a valid QUANTITY: " + value, value.matches(QUANTITY_PATTERN)); + } + @Test public void testBuildTransactionResultWithBlock() { SmartContractOuterClass.TriggerSmartContract.Builder builder2 = @@ -49,6 +57,8 @@ public void testBuildTransactionResultWithBlock() { transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x1"); Assert.assertEquals(transactionResult.getGas(), "0x64"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } @Test @@ -65,7 +75,8 @@ public void testBuildTransactionResult() { Assert.assertEquals("0x5691531881bc44adbc722060d85fdf29265823db8e884b0d104fcfbba253cf11", transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x"); - Assert.assertEquals(transactionResult.getNonce(), "0x0000000000000000"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } } From 709e1c3c2bfbb93db20f945bb12b687990782ca9 Mon Sep 17 00:00:00 2001 From: "wayne.zhang" <43207186+waynercheung@users.noreply.github.com> Date: Fri, 8 May 2026 11:08:40 +0800 Subject: [PATCH 40/43] feat(api): support int64_as_string parameter for GET requests (#6699) * feat(api): support int64_as_string parameter for GET requests (#6568) Add an opt-in `int64_as_string` query parameter on TRON HTTP GET endpoints. When set, int64/uint64 protobuf fields in the response are serialized as quoted JSON strings to avoid precision loss in clients whose native number type cannot safely represent integers above 2^53 - 1 (e.g. JavaScript). Scope: GET only. POST is intentionally unsupported because reading the request body in a centralized location (RateLimiterServlet.service or a Filter) would consume request.getReader() and break downstream servlets that read the body themselves. Most TRON query endpoints support both GET and POST, so clients that need precision can use the GET form. POST- only write endpoints return Transaction proto whose int64 fields would break round-trip JsonFormat.merge if quoted, so they should not enable this flag in the first place. - JsonFormat: add INT64_AS_STRING ThreadLocal + setInt64AsString / clearInt64AsString / isInt64AsString helpers; split printFieldValue INT64/SINT64/SFIXED64 and UINT64/FIXED64 branches so they emit quoted strings only when the flag is set. - Util: add INT64_AS_STRING constant + getInt64AsString (URL query, mirrors getVisible). - RateLimiterServlet.service: set ThreadLocal from URL query on GET only; clear in finally so reused Tomcat threads do not leak state across requests. - GetBurnTrx / GetPendingSize / GetTransactionCountByBlockNum: emit quoted int64 in their hand-built JSON responses when isInt64AsString is true. - JsonFormatInt64AsStringTest: covers default behavior, int64 / uint64 quoting, non-int64 fields unaffected, nested / map / boundary values (2^53 +/- 1, Long.MAX/MIN, -1), state cleanup (normal close, after exception, explicit clear), thread isolation, thread-reuse anti-pollution. Backward compatibility: requests without int64_as_string=true produce byte-identical responses to develop -- the new code paths are gated entirely on the new flag. Closes #6568. * refactor(api): address review feedback on int64_as_string PR Three small adjustments per review on PR #6699: - GetTransactionCountByBlockNumServlet: add trailing newline at end of file to satisfy checkstyle. - Util.getInt64AsString: align control flow with the existing Util.getVisible (single-return via local boolean, Boolean.valueOf instead of Boolean.parseBoolean). Functionally identical -- both return true only when the parameter value is "true" (case-insensitive). - Util.INT64_AS_STRING -> Util.INT64_AS_STRING_PARAM: rename the public parameter-name constant to avoid potential confusion with the unrelated private ThreadLocal field of the same simple name in JsonFormat. The user-facing query parameter remains "int64_as_string" -- only the Java identifier changes. --- .../core/services/http/GetBurnTrxServlet.java | 5 +- .../services/http/GetPendingSizeServlet.java | 5 +- .../core/services/http/GetRewardServlet.java | 5 +- .../GetTransactionCountByBlockNumServlet.java | 7 +- .../tron/core/services/http/JsonFormat.java | 58 +++- .../services/http/RateLimiterServlet.java | 10 + .../org/tron/core/services/http/Util.java | 16 ++ .../http/JsonFormatInt64AsStringTest.java | 264 ++++++++++++++++++ .../http/RateLimiterServletInt64Test.java | 164 +++++++++++ 9 files changed, 525 insertions(+), 9 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java create mode 100644 framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java diff --git a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java index e574affff6b..ea066a6e98c 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java @@ -19,7 +19,10 @@ public class GetBurnTrxServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getDynamicPropertiesStore().getBurnTrxAmount(); - response.getWriter().println("{\"burnTrxAmount\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"burnTrxAmount\": \"" + value + "\"}" + : "{\"burnTrxAmount\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java index 7e1a5f71841..9788c926586 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java @@ -19,7 +19,10 @@ public class GetPendingSizeServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getPendingSize(); - response.getWriter().println("{\"pendingSize\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"pendingSize\": \"" + value + "\"}" + : "{\"pendingSize\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java index c4d97f46c57..61b88d1160f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java @@ -24,7 +24,10 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { if (address != null) { value = manager.getMortgageService().queryReward(address); } - response.getWriter().println("{\"reward\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"reward\": \"" + value + "\"}" + : "{\"reward\": " + value + "}"; + response.getWriter().println(out); } catch (DecoderException | IllegalArgumentException e) { try { response.getWriter() diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java index e096df507d7..81c1ece73fb 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java @@ -40,6 +40,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) private void fillResponse(long num, HttpServletResponse response) throws IOException { long count = wallet.getTransactionCountByBlockNum(num); - response.getWriter().println("{\"count\": " + count + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"count\": \"" + count + "\"}" + : "{\"count\": " + count + "}"; + response.getWriter().println(out); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 96dedb1e20c..8a8c66fb371 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -90,6 +90,41 @@ public class JsonFormat { BalanceContract.TransactionBalanceTrace.class ); + /** + * Thread-local flag controlling whether int64/uint64 fields are serialized as JSON strings. + * Set via {@link #setInt64AsString(boolean)} early in request handling and cleared via + * {@link #clearInt64AsString()} in a finally block. Centralized in + * {@code RateLimiterServlet.service} for GET requests. Does not support nested scopes. + */ + private static final ThreadLocal INT64_AS_STRING = + ThreadLocal.withInitial(() -> false); + + /** + * Set whether int64/uint64 protobuf fields are serialized as quoted JSON strings to avoid + * precision loss in clients whose native number type cannot safely represent integers above + * 2^53 - 1 (e.g. JavaScript). Must be paired with {@link #clearInt64AsString()} in a + * finally block. + */ + public static void setInt64AsString(boolean enabled) { + INT64_AS_STRING.set(enabled); + } + + /** + * Clear the int64-as-string thread-local. Always call from a finally block to avoid + * polluting subsequent requests on the same (reused) thread. + */ + public static void clearInt64AsString() { + INT64_AS_STRING.remove(); + } + + /** + * Whether the current thread is in int64-as-string mode. Used by servlets that build + * JSON literals manually (i.e. do not go through {@link #printToString}). + */ + public static boolean isInt64AsString() { + return INT64_AS_STRING.get(); + } + /** * Outputs a textual representation of the Protocol Message supplied into the parameter output. * (This representation is the new version of the classic "ProtocolPrinter" output from the @@ -340,11 +375,8 @@ private static void printFieldValue(FieldDescriptor field, Object value, throws IOException { switch (field.getType()) { case INT32: - case INT64: case SINT32: - case SINT64: case SFIXED32: - case SFIXED64: case FLOAT: case DOUBLE: case BOOL: @@ -352,6 +384,18 @@ private static void printFieldValue(FieldDescriptor field, Object value, generator.print(value.toString()); break; + case INT64: + case SINT64: + case SFIXED64: + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(value.toString()); + generator.print("\""); + } else { + generator.print(value.toString()); + } + break; + case UINT32: case FIXED32: generator.print(unsignedToString((Integer) value)); @@ -359,7 +403,13 @@ private static void printFieldValue(FieldDescriptor field, Object value, case UINT64: case FIXED64: - generator.print(unsignedToString((Long) value)); + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(unsignedToString((Long) value)); + generator.print("\""); + } else { + generator.print(unsignedToString((Long) value)); + } break; case STRING: diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 80e62f38616..f173fbcaa82 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -116,6 +116,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) ? MetricLabels.UNDEFINED : contextPath + req.getServletPath(); + // int64_as_string is honored only on GET requests (URL query). POST is intentionally + // unsupported because reading the body here would consume request.getReader() and + // break downstream servlets that read it themselves. + if ("GET".equalsIgnoreCase(req.getMethod())) { + JsonFormat.setInt64AsString(Util.getInt64AsString(req)); + } try { resp.setContentType("application/json; charset=utf-8"); @@ -133,6 +139,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) } catch (Exception unexpected) { logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected); } finally { + // CRITICAL: this clear pairs with the setInt64AsString call above. Removing it + // will leak int64_as_string state across requests on reused Tomcat threads, + // producing intermittent quoted/unquoted output that is very hard to debug. + JsonFormat.clearInt64AsString(); if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) { ((IPreemptibleRateLimiter) rateLimiter).release(); } diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index a0e8c77d646..2e3331b2359 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -66,6 +66,7 @@ public class Util { public static final String PERMISSION_ID = "Permission_id"; public static final String VISIBLE = "visible"; + public static final String INT64_AS_STRING_PARAM = "int64_as_string"; public static final String TRANSACTION = "transaction"; public static final String TRANSACTION_EXTENSION = "transactionExtension"; public static final String VALUE = "value"; @@ -348,6 +349,21 @@ public static boolean existVisible(final HttpServletRequest request) { return Objects.nonNull(request.getParameter(VISIBLE)); } + /** + * Read int64_as_string from URL query parameter. Mirrors + * {@link #getVisible(HttpServletRequest)}. The flag is honored only on GET requests + * (read by {@link RateLimiterServlet#service}); POST requests do not support it + * because that would require caching the request body to allow re-reading by + * downstream servlets. + */ + public static boolean getInt64AsString(final HttpServletRequest request) { + boolean int64AsString = false; + if (StringUtil.isNotBlank(request.getParameter(INT64_AS_STRING_PARAM))) { + int64AsString = Boolean.valueOf(request.getParameter(INT64_AS_STRING_PARAM)); + } + return int64AsString; + } + public static boolean getVisiblePost(final String input) { boolean visible = false; if (StringUtil.isNotBlank(input)) { diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java new file mode 100644 index 00000000000..77ea73999d1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java @@ -0,0 +1,264 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import com.google.protobuf.UInt64Value; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; +import org.tron.protos.Protocol; + +/** + * Tests for {@link JsonFormat#setInt64AsString(boolean)} / + * {@link JsonFormat#clearInt64AsString()} / {@link JsonFormat#isInt64AsString()}. + * + *

Tron protos do not define uint64/fixed64 fields directly; all 64-bit values use int64. + * The uint64 branch is exercised using {@link com.google.protobuf.UInt64Value}, a protobuf + * well-known wrapper with a single {@code uint64 value} field. + */ +public class JsonFormatInt64AsStringTest { + + /** Defensive cleanup in case a test leaves the ThreadLocal dirty. */ + @After + public void clearState() { + JsonFormat.clearInt64AsString(); + } + + @Test + public void defaultBehaviorUnchangedWhenUnset() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + String out = JsonFormat.printToString(account, true); + assertTrue("expected unquoted balance, got: " + out, + out.contains("\"balance\":123456789012345") + || out.contains("\"balance\": 123456789012345")); + assertFalse("balance should not be quoted by default, got: " + out, + out.contains("\"balance\":\"123456789012345\"") + || out.contains("\"balance\": \"123456789012345\"")); + } + + @Test + public void int64FieldQuotedWhenSet() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("expected quoted balance, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64FieldQuotedWhenSet() { + UInt64Value v = UInt64Value.of(9007199254740993L); // 2^53 + 1 + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(v, true); + assertTrue("expected quoted uint64 value, got: " + out, + out.contains("\"9007199254740993\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64DefaultUnquoted() { + UInt64Value v = UInt64Value.of(9007199254740993L); + String out = JsonFormat.printToString(v, true); + assertTrue("expected unquoted uint64 value, got: " + out, + out.contains("9007199254740993")); + assertFalse("uint64 should not be quoted by default, got: " + out, + out.contains("\"9007199254740993\"")); + } + + @Test + public void stringBytesEnumNotAffected() { + // Note: proto3 does not serialize default-valued fields, so enum/bytes fields are + // set to non-default values to verify they appear in the output. + Protocol.Account account = Protocol.Account.newBuilder() + .setAccountName(ByteString.copyFromUtf8("alice")) + .setType(Protocol.AccountType.AssetIssue) // non-default enum value + .setBalance(1L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + // balance int64 should be quoted + assertTrue("balance should be quoted, got: " + out, out.contains("\"1\"")); + // enum type serialized by name (not a number), not affected by int64_as_string + assertTrue("enum type should appear as name, got: " + out, + out.contains("AssetIssue")); + // bytes account_name should still serialize normally + assertTrue("account_name should appear, got: " + out, out.contains("account_name")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void nestedInt64FieldsQuoted() { + Protocol.Block block = Protocol.Block.newBuilder() + .setBlockHeader(Protocol.BlockHeader.newBuilder() + .setRawData(Protocol.BlockHeader.raw.newBuilder() + .setNumber(9007199254740993L) // 2^53 + 1 + .setTimestamp(1700000000000L) + .build()) + .build()) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(block, true); + assertTrue("nested number should be quoted, got: " + out, + out.contains("\"9007199254740993\"")); + assertTrue("nested timestamp should be quoted, got: " + out, + out.contains("\"1700000000000\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void mapStringInt64ValuesQuoted() { + Protocol.Account account = Protocol.Account.newBuilder() + .putAsset("USDT", 123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("map value should be quoted, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void boundaryValuesAllQuoted() { + // Note: proto3 does not serialize a field whose value equals its type default (0 for int64), + // so 0L is covered separately via defaultBehaviorUnchangedWhenUnset / uint64DefaultUnquoted + // (both use non-default values) and does not need an explicit quoted-output test. + long[] values = { + (1L << 53) - 1, // max safe JS integer + 1L << 53, // boundary + (1L << 53) + 1, // first unsafe + Long.MAX_VALUE, + Long.MIN_VALUE, + -1L + }; + for (long v : values) { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(v).build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("value=" + v + " expected quoted, got: " + out, + out.contains("\"" + v + "\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + } + + @Test + public void clearResetsState() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + JsonFormat.clearInt64AsString(); + String out = JsonFormat.printToString(account, true); + assertFalse("state should be cleared, got: " + out, out.contains("\"1\"")); + } + + @Test + public void clearInFinallySurvivesException() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + try { + throw new RuntimeException("boom"); + } catch (RuntimeException expected) { + // expected + } finally { + JsonFormat.clearInt64AsString(); + } + String out = JsonFormat.printToString(account, true); + assertFalse("state leaked after exception, got: " + out, out.contains("\"1\"")); + } + + @Test + public void isInt64AsStringReflectsCurrentState() { + assertFalse(JsonFormat.isInt64AsString()); + JsonFormat.setInt64AsString(true); + try { + assertTrue(JsonFormat.isInt64AsString()); + } finally { + JsonFormat.clearInt64AsString(); + } + assertFalse(JsonFormat.isInt64AsString()); + } + + @Test + public void threadIsolation() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + final CountDownLatch barrier = new CountDownLatch(2); + ExecutorService ex = Executors.newFixedThreadPool(2); + try { + Future trueThread = ex.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + Future falseThread = ex.submit(() -> { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + }); + String withSet = trueThread.get(5, TimeUnit.SECONDS); + String noSet = falseThread.get(5, TimeUnit.SECONDS); + assertTrue("trueThread should see quoted: " + withSet, + withSet.contains("\"1\"")); + assertFalse("falseThread should see unquoted: " + noSet, + noSet.contains("\"1\"")); + } finally { + ex.shutdownNow(); + } + } + + @Test + public void noPollutionOnThreadReuse() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + ExecutorService single = Executors.newSingleThreadExecutor(); + try { + Future firstRun = single.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + assertTrue(firstRun.get(5, TimeUnit.SECONDS).contains("\"1\"")); + + // Reuse the same thread; without a new set, state must be cleared. + Future secondRun = single.submit(() -> JsonFormat.printToString(account, true)); + String second = secondRun.get(5, TimeUnit.SECONDS); + assertFalse("thread reuse leaked quoted state: " + second, + second.contains("\"1\"")); + } finally { + single.shutdownNow(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java new file mode 100644 index 00000000000..882c5f99833 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java @@ -0,0 +1,164 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; +import javax.annotation.Resource; +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.BaseTest; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +/** + * End-to-end integration tests for {@link RateLimiterServlet#service} wiring of the + * {@code int64_as_string} flag. The single-class {@link JsonFormatInt64AsStringTest} verifies + * the {@code JsonFormat} ThreadLocal mechanism in isolation; this test verifies the full + * request-handling chain: URL query --> {@code service()} --> ThreadLocal --> output, and + * the {@code finally} clear that prevents state leakage across reused threads. + * + *

Pins four contracts: + *

    + *
  1. GET with {@code ?int64_as_string=true} produces quoted int64 fields.
  2. + *
  3. GET without the flag produces unquoted int64 fields (regression baseline).
  4. + *
  5. POST never honors the flag, regardless of source -- GET-only is the documented + * contract under issue #6568.
  6. + *
  7. {@code service()}'s {@code finally} block clears the ThreadLocal so reused Tomcat + * threads do not leak state between requests.
  8. + *
+ * + *

Uses {@link GetNowBlockServlet} as the fixture servlet because its response goes through + * {@code JsonFormat.printToString}, which is what the ThreadLocal actually controls. + */ +public class RateLimiterServletInt64Test extends BaseTest { + + @Resource(name = "getNowBlockServlet") + private GetNowBlockServlet servlet; + + @Resource(name = "getBurnTrxServlet") + private GetBurnTrxServlet handBuiltServlet; + + static { + Args.setParam( + new String[]{ + "--output-directory", dbPath(), + }, TestConstants.TEST_CONF + ); + } + + @Before + public void clearBefore() { + JsonFormat.clearInt64AsString(); + } + + @After + public void clearAfter() { + JsonFormat.clearInt64AsString(); + } + + /** Contract 1: GET with int64_as_string=true on URL query produces quoted int64 fields. */ + @Test + public void getWithUrlFlagQuotesInt64() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** Contract 2: GET without flag produces unquoted int64 fields (default behavior). */ + @Test + public void getWithoutFlagKeepsUnquoted() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be unquoted when no flag, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\\d+.*")); + } + } + + /** + * Contract 3: POST never honors int64_as_string, regardless of where the flag is placed. + * Pins the GET-only design contract for issue #6568. Any future PR that tries to extend + * support to POST will fail this test, forcing an explicit design review. + */ + @Test + public void postWithUrlFlagIgnored() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertFalse("POST URL flag must be ignored under GET-only design, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** + * Contract 4 (CRITICAL): service() must clear the ThreadLocal in finally. Without this + * clear, reused Tomcat threads leak the flag across requests, producing intermittent + * quoted/unquoted output that is extremely hard to debug in production. + */ + @Test + public void serviceClearsThreadLocalInFinally() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + servlet.service(request, new MockHttpServletResponse()); + assertFalse( + "RateLimiterServlet.service must clear int64_as_string ThreadLocal in its finally " + + "block. Removing this clear will leak state across requests on reused threads.", + JsonFormat.isInt64AsString()); + } + + /** + * Contract 5: hand-built JSON servlets (the ones that emit JSON literals manually instead + * of going through {@link JsonFormat#printToString}) honor the flag. The previous tests use + * {@link GetNowBlockServlet} which goes through {@code printToString}; this test uses + * {@link GetBurnTrxServlet} as a representative of the four ternary-style servlets + * (GetBurnTrx / GetPendingSize / GetTransactionCountByBlockNum / GetReward) to lock down + * their {@code isInt64AsString() ? quoted : unquoted} branch -- so a future refactor that + * inverts the ternary or breaks the quote placement fails visibly here. + */ + @Test + public void handBuiltJsonServletQuotesInt64WhenFlagSet() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\"\\d+\".*")); + } + + /** Contract 6: hand-built JSON servlets default to unquoted output (regression baseline). */ + @Test + public void handBuiltJsonServletKeepsUnquotedByDefault() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be unquoted by default, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\\d+.*")); + } + + private String readBody(MockHttpServletResponse response) throws UnsupportedEncodingException { + return response.getContentAsString(); + } +} From 93629613b9a8599b105adbbf08699c6ba0f799a2 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Fri, 8 May 2026 11:32:15 +0800 Subject: [PATCH 41/43] fix(jsonrpc): harden ABI parser bounds and revert reason decoding (#6711) --- .../logsfilter/ContractEventParser.java | 20 +- .../services/jsonrpc/TronJsonRpcImpl.java | 49 +++- .../common/logsfilter/EventParserTest.java | 169 +++++++++++ .../JsonRpcCallAndEstimateGasTest.java | 277 ++++++++++++++++++ .../jsonrpc/TronJsonRpcRevertReasonTest.java | 97 ++++++ 5 files changed, 591 insertions(+), 21 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java create mode 100644 framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java diff --git a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java index 48181cb1255..ceefa9a8cae 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java +++ b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java @@ -1,8 +1,7 @@ package org.tron.common.logsfilter; -import static org.tron.common.math.Maths.min; - import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -38,9 +37,14 @@ public static String parseDataBytes(byte[] data, String typeStr, int index) { byte[] lengthBytes = subBytes(data, start, DATAWORD_UNIT_SIZE); // this length is byte count. no need X 32 int length = intValueExact(lengthBytes); + if (length < 0) { + throw new OutputLengthException("data length:" + length); + } byte[] realBytes = length > 0 ? subBytes(data, start + DATAWORD_UNIT_SIZE, length) : new byte[0]; - return type == Type.STRING ? new String(realBytes) : Hex.toHexString(realBytes); + return type == Type.STRING + ? new String(realBytes, StandardCharsets.UTF_8) + : Hex.toHexString(realBytes); } } catch (OutputLengthException | ArithmeticException e) { logger.debug("parseDataBytes ", e); @@ -74,11 +78,15 @@ protected static Integer intValueExact(byte[] data) { } protected static byte[] subBytes(byte[] src, int start, int length) { - if (ArrayUtils.isEmpty(src) || start >= src.length || length < 0) { - throw new OutputLengthException("data start:" + start + ", length:" + length); + if (ArrayUtils.isEmpty(src)) { + throw new OutputLengthException("source data is empty"); + } + if (start < 0 || start >= src.length || length < 0 || length > src.length - start) { + throw new OutputLengthException( + "data start:" + start + ", length:" + length + ", src.length:" + src.length); } byte[] dst = new byte[length]; - System.arraycopy(src, start, dst, 0, min(length, src.length - start, true)); + System.arraycopy(src, start, dst, 0, length); return dst; } 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 8c68f73fbc6..663b39de290 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 @@ -167,6 +167,8 @@ public enum RequestSource { private static final String NO_BLOCK_HEADER_BY_HASH = "header for hash not found"; 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; /** * thread pool of query section bloom store */ @@ -483,6 +485,36 @@ private void estimateEnergy(byte[] ownerAddressByte, byte[] contractAddressByte, estimateBuilder.setResult(retBuilder); } + /** + * Decodes an Error(string) revert reason when possible. + * Returns ": reason" for a non-empty reason, otherwise "". + */ + static String tryDecodeRevertReason(byte[] resData) { + if (resData == null || resData.length <= REVERT_REASON_SELECTOR_LENGTH) { + return ""; + } + if (!Hex.toHexString(resData, 0, REVERT_REASON_SELECTOR_LENGTH).equals(ERROR_SELECTOR)) { + return ""; + } + + int revertPayloadLength = resData.length - REVERT_REASON_SELECTOR_LENGTH; + if (revertPayloadLength > MAX_REVERT_REASON_PAYLOAD_BYTES) { + logger.debug("skip parsing oversized revert reason payload: {} bytes", revertPayloadLength); + return ""; + } + + try { + String reason = ContractEventParser.parseDataBytes( + Arrays.copyOfRange(resData, REVERT_REASON_SELECTOR_LENGTH, + resData.length), + "string", 0); + return reason.isEmpty() ? "" : ": " + reason; + } catch (RuntimeException e) { + logger.debug("parse revert reason failed", e); + return ""; + } + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 @@ -526,14 +558,8 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va } result = ByteArray.toJsonHex(listBytes); } else { - String errMsg = retBuilder.getMessage().toStringUtf8(); byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - if (resData.length > 4 && Hex.toHexString(resData).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(resData, 4, resData.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); if (resData.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); @@ -666,15 +692,8 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept } if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - String errMsg = retBuilder.getMessage().toStringUtf8(); - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - if (data.length > 4 && Hex.toHexString(data).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(data, 4, data.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); if (data.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java index 0d2a8ed1496..8e6b366fef8 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -5,6 +5,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.bouncycastle.crypto.OutputLengthException; import org.bouncycastle.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -100,6 +101,91 @@ public synchronized void testEventParser() { } + @Test + public void testParseDataBytesIntegerTypes() { + // uint256 = 255 + byte[] uintData = ByteArray.fromHexString( + "00000000000000000000000000000000000000000000000000000000000000ff"); + Assert.assertEquals("255", ContractEventParser.parseDataBytes(uintData, "uint256", 0)); + + // int256 = -1 (two's complement 0xFF..FF is signed negative one) + byte[] negIntData = ByteArray.fromHexString( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + Assert.assertEquals("-1", ContractEventParser.parseDataBytes(negIntData, "int256", 0)); + + // trcToken is classified as INT_NUMBER + byte[] tokenData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000064"); + Assert.assertEquals("100", ContractEventParser.parseDataBytes(tokenData, "trcToken", 0)); + } + + @Test + public void testParseDataBytesBool() { + byte[] trueData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("true", ContractEventParser.parseDataBytes(trueData, "bool", 0)); + + byte[] falseData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("false", ContractEventParser.parseDataBytes(falseData, "bool", 0)); + } + + @Test + public void testParseDataBytesFixedBytes() { + String hex = "1234567890abcdef0000000000000000000000000000000000000000000000ff"; + byte[] data = ByteArray.fromHexString(hex); + Assert.assertEquals(hex, ContractEventParser.parseDataBytes(data, "bytes32", 0)); + } + + @Test + public void testParseDataBytesAddress() { + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + // last 20 bytes = ca35...733c => Base58Check = TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt + byte[] data = ByteArray.fromHexString( + "000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c"); + Assert.assertEquals("TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt", + ContractEventParser.parseDataBytes(data, "address", 0)); + } + + @Test + public void testParseDataBytesDynamicBytes() { + // offset 0x20 | length 3 | 0x010203 padded to 32 bytes + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0102030000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("010203", ContractEventParser.parseDataBytes(data, "bytes", 0)); + } + + @Test + public void testParseDataBytesEmptyString() { + // offset 0x20 | length 0 + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesNonEmptyString() { + // "hello world" is 11 ASCII bytes (68656c6c6f20776f726c64), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000b" + + "68656c6c6f20776f726c64000000000000000000000000000000000000000000"); + Assert.assertEquals("hello world", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesMultiByteUtf8String() { + // "中文" UTF-8 = e4b8ad e69687 (6 bytes), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000006" + + "e4b8ade696870000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("中文", ContractEventParser.parseDataBytes(data, "string", 0)); + } + @Test public void testParseRevert() { String dataHex = "08c379a0" @@ -113,4 +199,87 @@ public void testParseRevert() { Assert.assertEquals(msg, "not enough input value"); } + + @Test + public void testSubBytesRejectsOversizedLength() { + // Length must fit in the available source bytes. Reject instead of + // truncating so oversized ABI lengths are not silently coerced. + byte[] src = new byte[]{1, 2, 3}; + try { + ContractEventParser.subBytes(src, 0, Integer.MAX_VALUE); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:0")); + Assert.assertTrue(e.getMessage().contains("length:2147483647")); + Assert.assertTrue(e.getMessage().contains("src.length:3")); + } + } + + @Test + public void testSubBytesAcceptsExactLength() { + byte[] src = new byte[]{1, 2, 3, 4}; + byte[] result = ContractEventParser.subBytes(src, 1, 3); + Assert.assertArrayEquals(new byte[]{2, 3, 4}, result); + } + + @Test + public void testSubBytesRejectsNegativeOffset() { + // ABI offsets are unsigned, but BigInteger(byte[]) interprets 0xFF..FF as + // -1. The guard should reject that value before System.arraycopy runs. + byte[] src = new byte[]{1, 2, 3, 4}; + try { + ContractEventParser.subBytes(src, -1, 3); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:-1")); + Assert.assertTrue(e.getMessage().contains("length:3")); + Assert.assertTrue(e.getMessage().contains("src.length:4")); + } + } + + @Test + public void testSubBytesRejectsEmptySource() { + try { + ContractEventParser.subBytes(new byte[0], 0, 0); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("source data is empty")); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeOffset() { + // End-to-end check: an offset field of 0xFF..FF decodes to -1 and should + // be rejected through the existing UnsupportedOperationException path. + String dataHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsMalformedLength() { + // ABI-encoded "string" whose declared length exceeds the available payload + // should be rejected via the existing UnsupportedOperationException path. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeLength() { + // ABI length is an unsigned word. If 0xFF..FF is decoded as -1, reject it + // instead of treating it as an empty string/bytes payload. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java new file mode 100644 index 00000000000..65defdab2ed --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -0,0 +1,277 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.EstimateEnergyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.TransactionExtention; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcInternalException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class JsonRpcCallAndEstimateGasTest { + + private static final String ERROR_REVERT_HEX = "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"; + private static final String REVERT_MSG = "REVERT opcode executed"; + private static final String MOCK_FROM_ADDRESS = "0x0000000000000000000000000000000000000000"; + private static final String MOCK_TO_ADDRESS = "0x0000000000000000000000000000000000000001"; + + private enum EstimatePath { + CONSTANT_CALL, + ESTIMATE_ENERGY + } + + private final boolean originalEstimateEnergy = CommonParameter.getInstance().isEstimateEnergy(); + private TronJsonRpcImpl mockRpc; + + @After + public void tearDown() throws Exception { + if (mockRpc != null) { + mockRpc.close(); + mockRpc = null; + } + CommonParameter.getInstance().setEstimateEnergy(originalEstimateEnergy); + } + + @Test + public void testGetCallAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception { + byte[] panicData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + + mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exception { + long energyRequired = 0x4321L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyRequired, + EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); + } + + @Test + public void testGetCallReturnsConstantResult() throws Exception { + byte[] part1 = ByteArray.fromHexString("deadbeef"); + byte[] part2 = ByteArray.fromHexString("cafebabe"); + + mockRpc = newRpcWithMockedSuccessfulCall(part1, part2); + + String result = mockRpc.getCall(newCallArgs(), "latest"); + + Assert.assertEquals("0xdeadbeefcafebabe", result); + } + + @Test + public void testEstimateGasReturnsEnergyUsed() throws Exception { + long energyUsed = 0x1234L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyUsed, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyUsed), result); + } + + private static CallArguments newCallArgs() { + CallArguments args = new CallArguments(); + args.setFrom(MOCK_FROM_ADDRESS); + args.setTo(MOCK_TO_ADDRESS); + args.setValue("0x0"); + args.setData("0x"); + return args; + } + + private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + estimateBuilder.setResult(retBuilder); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + for (byte[] bytes : constantResults) { + extBuilder.addConstantResult(ByteString.copyFrom(bytes)); + } + extBuilder.setEnergyUsed(0L); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long energyValue, + EstimatePath path) throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + estimateBuilder.setEnergyRequired(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + extBuilder.setEnergyUsed(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java new file mode 100644 index 00000000000..8d72aeed04f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java @@ -0,0 +1,97 @@ +package org.tron.core.services.jsonrpc; + +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.utils.ByteArray; + +public class TronJsonRpcRevertReasonTest { + + @Test + public void testTryDecodeRevertReasonWithMalformedLength() { + // Error(string) selector + offset=0x20 + length=0x7FFFFFFF + 3 bytes of payload. + // parseDataBytes throws because the declared length exceeds the buffer. + // The helper should return "" and leave the raw revert hex untouched. + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNegativeLength() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithValidData() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"); + Assert.assertEquals(": not enough input value", + TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithEmptyString() { + // require(cond, "") yields a empty string + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithOversizedPayload() { + // selector(4) + payload(4097) one byte over the 4096 limit: must be rejected before parse. + byte[] resData = new byte[4101]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNullData() { + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(null)); + } + + @Test + public void testTryDecodeRevertReasonWithShortSelector() { + // length == selector length (4): not enough bytes for any payload, reject. + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(new byte[]{ + 0x08, (byte) 0xc3, 0x79, (byte) 0xa0})); + } + + @Test + public void testTryDecodeRevertReasonWithNonErrorSelector() { + // Non-Error(string) selector (e.g. Panic(uint256) = 0x4e487b71) must be rejected. + byte[] resData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonAtPayloadLimit() { + // selector(4) + payload(4096) exactly at the limit: must go through parse, not size-reject. + byte[] resData = new byte[4100]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + // ABI offset = 0x20 + resData[4 + 31] = 0x20; + // ABI string length = 2 + resData[4 + 32 + 31] = 0x02; + // data "ok", remaining bytes stay zero-padded + resData[4 + 64] = 'o'; + resData[4 + 65] = 'k'; + Assert.assertEquals(": ok", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } +} From 5ef7de6ab91f3e92413b819aa1a66e3762e5900d Mon Sep 17 00:00:00 2001 From: Federico2014 Date: Fri, 8 May 2026 11:41:59 +0800 Subject: [PATCH 42/43] feat(crypto): shielded transaction API security enhancement (#6694) --- .../common/parameter/CommonParameter.java | 2 +- .../org/tron/core/config/args/NodeConfig.java | 2 +- .../tron/core/exception/ZksnarkException.java | 4 + common/src/main/resources/reference.conf | 2 +- .../tron/core/config/args/NodeConfigTest.java | 14 +- .../core/exception/ZksnarkExceptionTest.java | 29 ++ .../src/main/java/org/tron/core/Wallet.java | 422 +++++++++--------- .../zen/ShieldedTRC20ParametersBuilder.java | 11 +- .../tron/core/zen/ZenTransactionBuilder.java | 13 +- framework/src/main/resources/config.conf | 8 +- .../java/org/tron/core/ShieldWalletTest.java | 226 ++++++++++ .../tron/core/ShieldedTRC20BuilderTest.java | 10 + .../ShieldedTransferActuatorTest.java | 56 +++ .../org/tron/core/config/args/ArgsTest.java | 9 + .../core/services/RpcApiServicesTest.java | 1 + .../http/CreateSpendAuthSigServletTest.java | 15 + .../core/zksnark/MerkleContainerTest.java | 15 + sprout-verifying.key | Bin 1449 -> 0 bytes 18 files changed, 610 insertions(+), 229 deletions(-) create mode 100644 common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java delete mode 100644 sprout-verifying.key 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 e7957c917e2..8979b5ed68e 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -338,7 +338,7 @@ public class CommonParameter { @Getter @Setter - public boolean allowShieldedTransactionApi; // clearParam: true + public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter public long blockNumForEnergyLimit; 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 f9cf05a917f..8f7a39e02c0 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 @@ -86,7 +86,7 @@ public class NodeConfig { private boolean unsolidifiedBlockCheck = false; private int maxUnsolidifiedBlocks = 54; private String zenTokenId = "000000"; - private boolean allowShieldedTransactionApi = true; + private boolean allowShieldedTransactionApi = false; private double activeConnectFactor = 0.1; private double connectFactor = 0.6; // Legacy alias `maxActiveNodesWithSameIp` has no bean field: we only peek at it via diff --git a/common/src/main/java/org/tron/core/exception/ZksnarkException.java b/common/src/main/java/org/tron/core/exception/ZksnarkException.java index ec75e03852b..fab8019aebf 100644 --- a/common/src/main/java/org/tron/core/exception/ZksnarkException.java +++ b/common/src/main/java/org/tron/core/exception/ZksnarkException.java @@ -9,4 +9,8 @@ public ZksnarkException() { public ZksnarkException(String message) { super(message); } + + public ZksnarkException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 07f2114d061..4f2e061a65f 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -215,7 +215,7 @@ node { minParticipationRate = 0 # Whether to enable shielded transaction API - allowShieldedTransactionApi = true + allowShieldedTransactionApi = false # Whether to print config log at startup openPrintLog = true diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index fb22029262e..a52c51c1ba4 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -284,26 +284,26 @@ public void testLegacyAliasTakesPriorityOverModernKey() { } @Test - public void testShieldedApiDefaultsToTrueWhenNeitherKeySet() { + public void testShieldedApiDefaultsToFalseWhenNeitherKeySet() { NodeConfig nc = NodeConfig.fromConfig(withRef()); - assertTrue(nc.isAllowShieldedTransactionApi()); + assertFalse(nc.isAllowShieldedTransactionApi()); } @Test public void testShieldedApiModernKeyRespected() { NodeConfig nc = NodeConfig.fromConfig( - withRef("node.allowShieldedTransactionApi = false")); - assertFalse(nc.isAllowShieldedTransactionApi()); + withRef("node.allowShieldedTransactionApi = true")); + assertTrue(nc.isAllowShieldedTransactionApi()); } @Test public void testShieldedApiLegacyKeyRespected() { - // Regression guard: reference.conf ships `allowShieldedTransactionApi = true`, which + // Regression guard: reference.conf ships `allowShieldedTransactionApi = false`, which // used to make the legacy-key fallback dead code. A user who only set the legacy key // must still have their value honored. NodeConfig nc = NodeConfig.fromConfig( - withRef("node.fullNodeAllowShieldedTransaction = false")); - assertFalse(nc.isAllowShieldedTransactionApi()); + withRef("node.fullNodeAllowShieldedTransaction = true")); + assertTrue(nc.isAllowShieldedTransactionApi()); } @Test diff --git a/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java new file mode 100644 index 00000000000..26fa8fdd99a --- /dev/null +++ b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java @@ -0,0 +1,29 @@ +package org.tron.core.exception; + +import org.junit.Assert; +import org.junit.Test; + +public class ZksnarkExceptionTest { + + @Test + public void testNoArgConstructor() { + ZksnarkException e = new ZksnarkException(); + Assert.assertNull(e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageConstructor() { + ZksnarkException e = new ZksnarkException("boom"); + Assert.assertEquals("boom", e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageAndCauseConstructor() { + Throwable cause = new ArithmeticException("overflow"); + ZksnarkException e = new ZksnarkException("wrapped", cause); + Assert.assertEquals("wrapped", e.getMessage()); + Assert.assertSame(cause, e.getCause()); + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index ba1c42f3d08..ce3c3ac68f1 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -256,7 +256,9 @@ @Component public class Wallet { - private static final String SHIELDED_ID_NOT_ALLOWED = "ShieldedTransactionApi is not allowed"; + private static final String SHIELDED_ID_NOT_ALLOWED = + "Shielded transaction API is disabled; " + + "set node.allowShieldedTransactionApi=true to enable."; private static final String PAYMENT_ADDRESS_FORMAT_WRONG = "paymentAddress format is wrong"; private static final String SHIELDED_TRANSACTION_SCAN_RANGE = "request requires start_block_index >= 0 && end_block_index > " @@ -746,7 +748,7 @@ public WitnessList getPaginatedNowWitnessList(long offset, long limit) throws if (limit > WITNESS_COUNT_LIMIT_MAX) { limit = WITNESS_COUNT_LIMIT_MAX; } - + /* In the maintenance period, the VoteStores will be cleared. To avoid the race condition of VoteStores deleted but Witness vote counts not updated, @@ -1468,8 +1470,8 @@ public Protocol.ChainParameters getChainParameters() { builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getAllowTvmSelfdestructRestriction") .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmSelfdestructRestriction()) - .build()); - + .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getProposalExpireTime") .setValue(dbManager.getDynamicPropertiesStore().getProposalExpireTime()) @@ -2274,58 +2276,58 @@ public TransactionCapsule createShieldedTransaction(PrivateParameters request) checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } - - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } - - // input - if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } + + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } + + // input + if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(expsk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(expsk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.build(); + return builder.build(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction except, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction except, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( @@ -2376,59 +2378,60 @@ public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } - // input - if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + // input + if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress( + note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(ak, + nsk, + ovk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(ak, - nsk, - ovk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.buildWithoutAsk(); + return builder.buildWithoutAsk(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction exception, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction exception, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } private void shieldedOutput(List shieldedReceives, @@ -2446,7 +2449,6 @@ private void shieldedOutput(List shieldedReceives, } } - public ShieldedAddressInfo getNewShieldedAddress() throws BadItemException, ZksnarkException { checkAllowShieldedTransactionApi(); @@ -3605,77 +3607,80 @@ public ShieldedTRC20Parameters createShieldedContractParameters( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), dbManager.getDynamicPropertiesStore().disableJavaLangMath())); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } - builder.setTransparentFromAmount(fromAmount); - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + builder.setTransparentFromAmount(fromAmount); + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20Input(builder, spendNote, expsk); - } + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20Input(builder, spendNote, expsk); + } - for (ReceiveNote receiveNote : shieldedReceives) { - buildShieldedTRC20Output(builder, receiveNote, ovk); - } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No valid transparent TRC-20 output address"); - } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No valid transparent TRC-20 output address"); + } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20Input(builder, spendNote, expsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20Input(builder, spendNote, expsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(true); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } - - return builder.build(true); } private void buildShieldedTRC20InputWithAK( @@ -3738,65 +3743,69 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), chainBaseManager.getDynamicPropertiesStore().disableJavaLangMath()); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - builder.setTransparentFromAmount(fromAmount); - ReceiveNote receiveNote = shieldedReceives.get(0); - buildShieldedTRC20Output(builder, receiveNote, ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - } - for (ReceiveNote receiveNote : shieldedReceives) { + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + builder.setTransparentFromAmount(fromAmount); + ReceiveNote receiveNote = shieldedReceives.get(0); buildShieldedTRC20Output(builder, receiveNote, ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No transparent TRC-20 output address"); + } + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No transparent TRC-20 output address"); - } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(false); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } - return builder.build(false); } private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddress, @@ -4543,4 +4552,3 @@ public PricesResponseMessage getMemoFeePrices() { return null; } } - diff --git a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java index 95e4eeb0ccd..4b980c7b7c9 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -14,6 +14,7 @@ import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.BytesMessage; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; @@ -547,8 +548,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -558,8 +559,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -570,23 +571,23 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public static class SpendDescriptionInfo { diff --git a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java index 2e531e44d44..fc3be8352ee 100644 --- a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java @@ -10,6 +10,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.zksnark.IncrementalMerkleVoucherContainer; import org.tron.common.zksnark.JLibrustzcash; @@ -66,8 +67,8 @@ public ZenTransactionBuilder() { } public void addSpend(SpendDescriptionInfo spendDescriptionInfo) { + valueBalance = StrictMathWrapper.addExact(valueBalance, spendDescriptionInfo.note.getValue()); spends.add(spendDescriptionInfo); - valueBalance += spendDescriptionInfo.note.getValue(); } public void addSpend( @@ -75,8 +76,8 @@ public void addSpend( Note note, byte[] anchor, IncrementalMerkleVoucherContainer voucher) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -85,8 +86,8 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -97,23 +98,23 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, ovk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void setTransparentInput(byte[] address, long value) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..05c7ab56378 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -178,7 +178,13 @@ node { minParticipationRate = 15 - # allowShieldedTransactionApi = true + # WARNING: Some shielded transaction APIs require sending private keys as parameters. + # Calling these APIs on untrusted or remote nodes may leak your private keys. + # It is recommended to invoke them locally for development and testing. + # To opt in, set: allowShieldedTransactionApi = true + # Migration: the legacy key node.fullNodeAllowShieldedTransaction is still supported + # but deprecated; please migrate to node.allowShieldedTransactionApi. + # allowShieldedTransactionApi = false # openPrintLog = true diff --git a/framework/src/test/java/org/tron/core/ShieldWalletTest.java b/framework/src/test/java/org/tron/core/ShieldWalletTest.java index b9fa48dca38..0353d260eff 100644 --- a/framework/src/test/java/org/tron/core/ShieldWalletTest.java +++ b/framework/src/test/java/org/tron/core/ShieldWalletTest.java @@ -4,15 +4,18 @@ import static org.mockito.Mockito.spy; import static org.tron.core.zen.ZksnarkInitService.librustzcashInitZksnarkParams; +import com.google.protobuf.ByteString; import java.math.BigInteger; import javax.annotation.Resource; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.PrivateParameters; import org.tron.api.GrpcAPI.PrivateParametersWithoutAsk; import org.tron.api.GrpcAPI.PrivateShieldedTRC20Parameters; import org.tron.api.GrpcAPI.PrivateShieldedTRC20ParametersWithoutAsk; +import org.tron.api.GrpcAPI.ReceiveNote; import org.tron.api.GrpcAPI.ShieldedAddressInfo; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; import org.tron.common.BaseTest; @@ -22,6 +25,7 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ZksnarkException; import org.tron.core.services.http.JsonFormat; import org.tron.core.services.http.JsonFormat.ParseException; @@ -450,4 +454,226 @@ public void testCreateShieldedContractParametersWithoutAsk() throws ContractExeE Assert.fail(); } } + + private static final byte[] SHIELDED_CONTRACT_ADDRESS = + ByteArray.fromHexString("4144007979359ECAC395BBD3CEF8060D3DF2DC3F01"); + private static final String VALID_PAYMENT_ADDR = + "ztron1y99u6ejqenupvfkp5g6q6yqkp0a44c48cta0dd5gejtqa4v27hqa2cghfvdxnmneh6qqq03fa75"; + + private Wallet newSpyWallet() throws ContractExeException { + Args.getInstance().setAllowShieldedTransactionApi(true); + Wallet wallet1 = spy(new Wallet()); + doReturn(BigInteger.valueOf(1).toByteArray()) + .when(wallet1).getShieldedContractScalingFactor(SHIELDED_CONTRACT_ADDRESS); + return wallet1; + } + + private GrpcAPI.SpendNoteTRC20 spendNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return GrpcAPI.SpendNoteTRC20.newBuilder() + .setNote(note) + .setAlpha(ByteString.copyFrom(new byte[32])) + .setRoot(ByteString.copyFrom(new byte[32])) + .setPath(ByteString.copyFrom(new byte[1024])) + .setPos(0) + .build(); + } + + private ReceiveNote receiveNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return ReceiveNote.newBuilder().setNote(note).build(); + } + + @Test + public void testCreateShieldedContractParameters_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No valid transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_MINT_emptyOvk() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .setFromAmount("100") + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + try { + ShieldedTRC20Parameters params = wallet1.createShieldedContractParametersWithoutAsk(request); + Assert.assertNotNull(params); + } catch (Exception e) { + Assert.fail("MINT with empty ovk should auto-generate one: " + e); + } + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } } diff --git a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java index c2c4bfe3006..71192706049 100644 --- a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java +++ b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.util.encoders.Hex; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Ignore; @@ -73,9 +74,18 @@ public class ShieldedTRC20BuilderTest extends BaseTest { VerifyTransferProof transferContract = new VerifyTransferProof(); VerifyBurnProof burnContract = new VerifyBurnProof(); + private static boolean origShieldedApi; + @BeforeClass public static void initZksnarkParams() { ZksnarkInitService.librustzcashInitZksnarkParams(); + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; } @Ignore diff --git a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java index faec4c74039..578f9f5ebed 100755 --- a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java @@ -1157,6 +1157,9 @@ public void publicAddressToShieldNoteValueFailure() { actuator.validate(); actuator.execute(ret); Assert.assertTrue(false); + } catch (ArithmeticException e) { + // StrictMathWrapper.subtractExact throws ArithmeticException on overflow + Assert.assertTrue(true); } catch (ContractValidateException e) { Assert.assertTrue(e instanceof ContractValidateException); Assert.assertEquals("librustzcashSaplingFinalCheck error", e.getMessage()); @@ -1346,5 +1349,58 @@ public void shieldAddressToPublic() { Assert.assertTrue(false); } } + + /** + * Test that shielded transfer transaction validation works even when + * allowShieldedTransactionApi is disabled. This verifies that the API flag + * only gates wallet/helper APIs, not the core transaction validation logic. + */ + @Test + public void shieldedTransferValidationWorksWhenApiDisabled() { + boolean orig = Args.getInstance().isAllowShieldedTransactionApi(); + // Disable the shielded API (this should NOT affect transaction validation) + Args.getInstance().setAllowShieldedTransactionApi(false); + + dbManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); + dbManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(AMOUNT); + + try { + ZenTransactionBuilder builder = new ZenTransactionBuilder(wallet); + SpendingKey sk = SpendingKey.random(); + ExpandedSpendingKey expsk = sk.expandedSpendingKey(); + PaymentAddress address = sk.defaultAddress(); + Note note = new Note(address, AMOUNT); + IncrementalMerkleVoucherContainer voucher = createSimpleMerkleVoucherContainer(note.cm()); + byte[] anchor = voucher.root().getContent().toByteArray(); + dbManager.getMerkleContainer() + .putMerkleTreeIntoStore(anchor, voucher.getVoucherCapsule().getTree()); + builder.addSpend(expsk, note, anchor, voucher); + + addZeroValueOutputNote(builder); + + long fee = dbManager.getDynamicPropertiesStore().getShieldedTransactionCreateAccountFee(); + String addressNotExist = + Wallet.getAddressPreFixString() + "8ba2aaae540c642e44e3bed5522c63bbc21f0000"; + + builder.setTransparentOutput(ByteArray.fromHexString(addressNotExist), AMOUNT - fee); + + TransactionCapsule transactionCap = builder.build(); + Contract contract = + transactionCap.getInstance().toBuilder().getRawDataBuilder().getContract(0); + ShieldedTransferActuator actuator = new ShieldedTransferActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()).setContract(contract) + .setTx(transactionCap); + + // Validation should succeed even when API is disabled + actuator.validate(); + } catch (ContractValidateException e) { + Assert.fail("Shielded transfer validation should not throw ContractValidateException: " + + e.getMessage()); + } catch (Exception e) { + Assert.fail("Shielded transfer should not throw Exception: " + e.getMessage()); + } finally { + Args.getInstance().setAllowShieldedTransactionApi(orig); + } + } } diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index e0d9d456e9a..45bc7c049e3 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -415,5 +415,14 @@ public void testFetchBlockTimeoutInRangeUnchanged() { Assert.assertEquals(500, Args.getInstance().getFetchBlockTimeout()); Args.clearParam(); } + + @Test + public void testAllowShieldedTransactionApiDefault() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + Assert.assertFalse(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(true); + Assert.assertTrue(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(false); + } } diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index 94b05cae98f..e87b2566205 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -149,6 +149,7 @@ public class RpcApiServicesTest { public static void init() throws IOException { Args.setParam(new String[] {"-d", temporaryFolder.newFolder().toString()}, TestConstants.TEST_CONF); + getInstance().allowShieldedTransactionApi = true; Assert.assertEquals(5, getInstance().getRpcMaxRstStream()); Assert.assertEquals(10, getInstance().getRpcSecondsPerWindow()); String OWNER_ADDRESS = Wallet.getAddressPreFixString() diff --git a/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java index 85d6764132b..d3ebf26a261 100644 --- a/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java @@ -8,7 +8,9 @@ import java.io.UnsupportedEncodingException; import javax.annotation.Resource; import org.apache.http.client.methods.HttpPost; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -18,6 +20,8 @@ public class CreateSpendAuthSigServletTest extends BaseTest { + private static boolean origShieldedApi; + static { Args.setParam( new String[]{ @@ -26,6 +30,17 @@ public class CreateSpendAuthSigServletTest extends BaseTest { ); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + @Resource private CreateSpendAuthSigServlet createSpendAuthSigServlet; diff --git a/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java index ed52e014a7b..61fb36a9f68 100644 --- a/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java @@ -3,7 +3,9 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import javax.annotation.Resource; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; @@ -38,10 +40,23 @@ public class MerkleContainerTest extends BaseTest { // private static MerkleContainer merkleContainer; + private static boolean origShieldedApi; + static { Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + /*@Before public void init() { merkleContainer = MerkleContainer diff --git a/sprout-verifying.key b/sprout-verifying.key deleted file mode 100644 index 16655abb5d779ce6818a9b75bbc0cd7a1ac7040b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1449 zcmV;a1y=en2U9s<3m5x<&kWAU?`%6S?TQhqNdm<^h*lR5`rT3+g~LVmr3UcUFWO%F{OCp?w%Gq%gRxie6r&EN$~{E1sKuJEA8!J;u8h} z+$5p;!Hp3-GzC|PweD|f5xSSI47o*|7W^*EG8oJwFglKid%gTgi?K}GWXx+|edOWQ z!fJKO7!xcaJ>PuR0NRfThiRg1zNXxk*m^;)w-$u1djpPd50jl)zm57*C@?L2BtUsa zqH2t9IGzsSOVJ$Zf6Vc#N?U?XG%H6keZv@r%dL*YgkcXXwf1Ng3UB+7=Uj z=$@K~%>{ah2Y#^GDWlV<_dg326NOgJYWT^zTN{-1-jS>vodDN==xVHmQx)+>0n7GX zyoj;z!%3)~v>58i>&}B#AtW%uW5LE=DvupvGHUNHtVW!VYCS?a8h`NtGHAtK#D){f zFcu}5B11|qE5wCe!Ms8?kp(M9ZPKjpuSWXO1!@+2*Mc|BP!gp8$s|80TEzQ|GpWsqG^Mqm##y0I`*se7+0%9Tlh#)2X!vv9FFk`6(`qm~P zU(QG#Fp9jep8~Z5pEz$%uBtZiH;5cICKmp_h7DsQ+rNhW5I}X=dr%5_;~?F2?7*_c zi!^mYddpPd8+5wa5AB;K|z`Z56R&36noQ zzJ`fAMeb7vX5ZKC4K$>2w_=CS&Yl}zE3-3>1u)B;EO=YS<(2xf_>OzBi)8uiE(bvP zT&_MGo{h-D;v%}qNKIxFpscSACTV9PY8Oqc3Pwx~Nr`eQxhA%cl%hccNz9mF2Y z1}`w-l8y`h;E|X8h$8kyE33QWKF)n+V)|Mmks2uET*@8OER|p*H=VQpA-p`63(r?X z4L{)iT;#Yw3B77C17aOH3ONce3NZ>Y3Ns2c3N;Eg3O5Qk3ONcezCAy}v#)x!4=HC_OwJMfn0`If3$w*TtSWT%C`K#2!eCz7Nznrz^knJIyc@NjPtw0 zb677uz}@@}YE$hp7BEM`2FgM4#2T8?e_hH_jE(cnZ#3c&Eo?Hd2QyCQ+#P+SILVt~ zYAjzhqmT(Alu^O{+q3yHzR6n^D$~w5bO|u-e$<~}qHS^-)l^0Cr^&=d1yw+_TMmvD z_10QZ;Km5 Date: Fri, 8 May 2026 12:46:02 +0800 Subject: [PATCH 43/43] feat(log): improve log for gRPC, DB, and shutdown (#6700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(log): bridge JUL to SLF4J and improve shutdown log reliability ### What Changed - gRPC now appear in the application log instead of going only to standard error. - Database size stats are no longer printed at info level during startup, which reduces log noise. - Shutdown now waits longer before stopping the log system, giving pending messages more time to flush. - Async appender now allows up to 5 seconds to drain its queue on shutdown. - Interrupted shutdowns now show a clearer log message. - Test log config is INFO level. ### Impact `✅ Fewer missed gRPC error logs` `✅ Cleaner startup output` `✅ Fewer lost shutdown logs` `✅ Fewer test logs` * docs(log): clarify resetJUL vs handler-removal responsibilities * refactor(log): clean up TronLogShutdownHook constants, timing, and javadoc * fix(log): fail fast on unreadable --log-config instead of silent fallback * feat(log): slow signature verifications above 50ms * feat(log): watchdog-warn when LevelDB open stalls * test(log): pin TronLogShutdownHook wait-window semantics * style(log): clarify LogService comment and tighten test local var casing * refactor(log): drop manifest probe from LevelDB open watchdog * feat(log): route grpc.* logs to dedicated grpc.log * fix(log): make grpc logger asynchronous * chore(log): register framework and plugin logger topics * chore(log): raise slow signature verification to warn --- build.gradle | 5 +- .../leveldb/LevelDbDataSourceImpl.java | 39 +++++ .../tron/common/storage/metric/DbStat.java | 2 +- .../tron/core/capsule/TransactionCapsule.java | 19 +++ .../java/org/tron/common/log/LogService.java | 41 +++++- .../common/parameter/CommonParameter.java | 14 ++ .../tron/core/config/TronLogShutdownHook.java | 29 +++- framework/src/main/resources/logback.xml | 71 ++++++++- .../org/tron/common/log/LogServiceTest.java | 138 ++++++++++++++++++ .../leveldb/LevelDbDataSourceImplTest.java | 67 +++++++++ .../core/capsule/TransactionCapsuleTest.java | 67 ++++++++- .../core/config/TronLogShutdownHookTest.java | 84 +++++++++++ .../tron/core/exception/TronErrorTest.java | 9 +- framework/src/test/resources/logback-test.xml | 7 +- gradle/verification-metadata.xml | 33 ++--- plugins/src/main/resources/logback.xml | 4 +- 16 files changed, 580 insertions(+), 49 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/log/LogServiceTest.java create mode 100644 framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java diff --git a/build.gradle b/build.gradle index 4ceebadb158..35bc557a583 100644 --- a/build.gradle +++ b/build.gradle @@ -81,8 +81,9 @@ subprojects { } dependencies { - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' - implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.25' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' + implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36' + implementation group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.36' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.13' implementation "com.google.code.findbugs:jsr305:3.0.0" implementation group: 'org.springframework', name: 'spring-context', version: "${springVersion}" diff --git a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java index c48800573e1..aa85ac08f45 100644 --- a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java +++ b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java @@ -32,6 +32,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -45,6 +48,7 @@ import org.iq80.leveldb.ReadOptions; import org.iq80.leveldb.WriteBatch; import org.iq80.leveldb.WriteOptions; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.WriteOptionsWrapper; import org.tron.common.storage.metric.DbStat; @@ -61,6 +65,11 @@ public class LevelDbDataSourceImpl extends DbStat implements DbSourceInter, Iterable>, Instance { + /** First watchdog WARN fires this many seconds after factory.open() begins. */ + private static final long OPEN_WATCHDOG_INITIAL_DELAY_SEC = 60; + /** Subsequent watchdog WARN lines are emitted on this interval. */ + private static final long OPEN_WATCHDOG_PERIOD_SEC = 30; + private String dataBaseName; private DB database; private volatile boolean alive; @@ -121,6 +130,14 @@ private void openDatabase(Options dbOptions) throws IOException { if (!Files.isSymbolicLink(dbPath.getParent())) { Files.createDirectories(dbPath.getParent()); } + final long openStartNs = System.nanoTime(); + ScheduledExecutorService watchdog = ExecutorServiceManager + .newSingleThreadScheduledExecutor("db-open-watchdog-" + dataBaseName, true); + ScheduledFuture watchdogTask = watchdog.scheduleAtFixedRate( + () -> logSlowOpen(dbPath, openStartNs), + OPEN_WATCHDOG_INITIAL_DELAY_SEC, + OPEN_WATCHDOG_PERIOD_SEC, + TimeUnit.SECONDS); try { DbSourceInter.checkOrInitEngine(getEngine(), dbPath.toString(), TronError.ErrCode.LEVELDB_INIT); @@ -139,6 +156,28 @@ private void openDatabase(Options dbOptions) throws IOException { logger.error("Open Database {} failed", dataBaseName, e); } throw new TronError(e, TronError.ErrCode.LEVELDB_INIT); + } finally { + watchdogTask.cancel(false); + watchdog.shutdownNow(); + } + } + + /** + * Emits a WARN when factory.open() is still blocked — usually because the + * MANIFEST has grown large enough to make replay expensive. + */ + void logSlowOpen(Path dbPath, long startNs) { + try { + long elapsedSec = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNs); + logger.warn("DB {} open still in progress after {}s. path={}. " + + "This startup will complete; to speed up future restarts, run " + + "`java -jar Toolkit.jar db archive -d {}` before the next startup " + + "to rebuild the MANIFEST (the tool requires an exclusive DB lock, " + + "so it cannot run while the node is up).", + dataBaseName, elapsedSec, dbPath, parentPath); + } catch (Exception e) { + // Purely observational - never let the watchdog disrupt startup. + logger.debug("db-open-watchdog failure for {}: {}", dataBaseName, e.getMessage()); } } diff --git a/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java b/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java index c7fecf2a351..eb0362ad2e9 100644 --- a/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java +++ b/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java @@ -17,7 +17,7 @@ protected void statProperty() { double size = Double.parseDouble(tmp[2]) * 1048576.0; Metrics.gaugeSet(MetricKeys.Gauge.DB_SST_LEVEL, files, getEngine(), getName(), level); Metrics.gaugeSet(MetricKeys.Gauge.DB_SIZE_BYTES, size, getEngine(), getName(), level); - logger.info("DB {}, level:{},files:{},size:{} M", + logger.debug("DB {}, level:{},files:{},size:{} M", getName(), level, files, size / 1048576.0); }); } catch (Exception e) { diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java index b11c6b1e0a4..bb4b70cde1b 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import lombok.Getter; import lombok.Setter; @@ -94,6 +95,8 @@ public class TransactionCapsule implements ProtoCapsule { .newFixedThreadPool(esName, CommonParameter.getInstance() .getValidContractProtoThreadNum()); private static final String OWNER_ADDRESS = "ownerAddress_"; + // 2-6 ms in general, so we set 50 ms as the threshold for slow signature verification. + private static final long SLOW_SIG_VERIFY_MS = 50; private Transaction transaction; @Setter @@ -648,6 +651,7 @@ public boolean validatePubSignature(AccountStore accountStore, byte[] hash = getTransactionId().getBytes(); + long startNs = System.nanoTime(); try { if (!validateSignature(this.transaction, hash, accountStore, dynamicPropertiesStore)) { isVerified = false; @@ -656,12 +660,27 @@ public boolean validatePubSignature(AccountStore accountStore, } catch (SignatureException | PermissionException | SignatureFormatException e) { isVerified = false; throw new ValidateSignatureException(e.getMessage()); + } finally { + logSlowSigVerify(startNs); } isVerified = true; } return true; } + /** + * WARN-logs when a single signature verification exceeds + * {@link #SLOW_SIG_VERIFY_MS}. Package-private so it can be exercised from + * tests without forcing a real slow crypto path. + */ + void logSlowSigVerify(long startNs) { + long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + if (costMs > SLOW_SIG_VERIFY_MS) { + logger.warn("slow verify: txId={}, sigCount={}, cost={} ms", + getTransactionId(), this.transaction.getSignatureCount(), costMs); + } + } + /** * validate signature */ diff --git a/common/src/main/java/org/tron/common/log/LogService.java b/common/src/main/java/org/tron/common/log/LogService.java index bce52001e92..fdeba534b5e 100644 --- a/common/src/main/java/org/tron/common/log/LogService.java +++ b/common/src/main/java/org/tron/common/log/LogService.java @@ -2,6 +2,8 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.jul.LevelChangePropagator; +import ch.qos.logback.classic.spi.LoggerContextListener; import ch.qos.logback.core.util.StatusPrinter; import java.io.File; import org.slf4j.LoggerFactory; @@ -12,18 +14,43 @@ public class LogService { public static void load(String path) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); try { - File file = new File(path); - if (!file.exists() || !file.isFile() || !file.canRead()) { - return; + // Fail fast rather than silently falling back to the classpath default — + // that legacy behavior misled operators into thinking their custom + // --log-config was active. + if (path != null && !path.isEmpty()) { + File file = new File(path); + if (!file.exists() || !file.isFile() || !file.canRead()) { + throw new IllegalArgumentException( + "logback config is not a readable file: " + path); + } + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(lc); + lc.reset(); + configurator.doConfigure(file); } - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(file); + // Whether we loaded a custom config via --log-config or kept the classpath + // default, make sure Logback level changes are propagated back to JUL so + // gRPC loggers actually honor the levels declared in the XML. If + // the active config already registered a LevelChangePropagator we leave + // it alone. + ensureLevelChangePropagator(lc); } catch (Exception e) { throw new TronError(e, TronError.ErrCode.LOG_LOAD); } finally { StatusPrinter.printInCaseOfErrorsOrWarnings(lc); } } + + private static void ensureLevelChangePropagator(LoggerContext lc) { + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + return; + } + } + LevelChangePropagator propagator = new LevelChangePropagator(); + propagator.setContext(lc); + propagator.setResetJUL(true); + propagator.start(); + lc.addListener(propagator); + } } 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 8979b5ed68e..493d508cd58 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -8,6 +8,7 @@ import java.util.Set; import lombok.Getter; import lombok.Setter; +import org.slf4j.bridge.SLF4JBridgeHandler; import org.tron.common.args.GenesisBlock; import org.tron.common.config.DbBackupConfig; import org.tron.common.cron.CronExpression; @@ -23,6 +24,19 @@ public class CommonParameter { + // Install the JUL->SLF4J bridge early so that JUL log records emitted during + // static init of grpc classes (or from unit tests that don't invoke + // LogService.load()) still reach Logback. + // removeHandlersForRootLogger() strips JUL's default ConsoleHandler so the + // same record is not emitted twice (once by JUL's own console output and + // once via the bridge to Logback). + static { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + if (!SLF4JBridgeHandler.isInstalled()) { + SLF4JBridgeHandler.install(); + } + } + protected static CommonParameter PARAMETER = new CommonParameter(); // Runtime chain state: set by VMConfig.initVmHardFork() diff --git a/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java b/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java index f497b9a85d8..4ee8d58483c 100644 --- a/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java +++ b/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java @@ -2,7 +2,6 @@ import ch.qos.logback.core.hook.ShutdownHookBase; import ch.qos.logback.core.util.Duration; -import org.tron.program.FullNode; /** * @author kiven @@ -16,11 +15,24 @@ public class TronLogShutdownHook extends ShutdownHookBase { private static final Duration CHECK_SHUTDOWN_DELAY = Duration.buildByMilliseconds(100); /** - * The check times before shutdown. default is 60000/100 = 600 times. + * Maximum time to wait for a graceful application shutdown before forcing + * a log flush. Each pool managed by ExecutorServiceManager.shutdownAndAwait- + * Termination() can take up to 120 s in the worst case (60 s await + + * shutdownNow + 60 s await). 180 s is therefore not a hard upper bound, but + * a pragmatic headroom that assumes the many pools in the node shut down + * largely in parallel; in pathological cases trailing shutdown logs may + * still be truncated. In practice 180 s of shutdown output is also enough + * to diagnose most stalls — if a pool is still alive past that window the + * earlier logs already carry the stack/trace context needed to locate the + * offender, so truncating the tail is an acceptable trade-off against + * holding JVM exit open indefinitely. */ - private final long check_times = 60 * 1000 / CHECK_SHUTDOWN_DELAY.getMilliseconds(); + private static final long MAX_WAIT_MS = 3 * 60 * 1000; - // if true, shutdown hook will be executed , for example, 'java -jar FullNode.jar -[v|h]'. + private static final long CHECK_TIMES = + MAX_WAIT_MS / CHECK_SHUTDOWN_DELAY.getMilliseconds(); + + // if true, shutdown hook will be executed, for example, 'java -jar FullNode.jar -[v|h]'. public static volatile boolean shutDown = true; public TronLogShutdownHook() { @@ -29,16 +41,19 @@ public TronLogShutdownHook() { @Override public void run() { try { - for (int i = 0; i < check_times; i++) { + for (long i = 0; i < CHECK_TIMES; i++) { if (shutDown) { break; } - addInfo("Sleeping for " + CHECK_SHUTDOWN_DELAY); + if (i % 100 == 0) { + long elapsedSeconds = i * CHECK_SHUTDOWN_DELAY.getMilliseconds() / 1000; + addInfo("Waiting for application shutdown... elapsed=" + elapsedSeconds + "s"); + } Thread.sleep(CHECK_SHUTDOWN_DELAY.getMilliseconds()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - addInfo("TronLogShutdownHook run error :" + e.getMessage()); + addInfo("TronLogShutdownHook interrupted: " + e.getMessage()); } super.stop(); } diff --git a/framework/src/main/resources/logback.xml b/framework/src/main/resources/logback.xml index 03d870e92e0..1b0955df2fd 100644 --- a/framework/src/main/resources/logback.xml +++ b/framework/src/main/resources/logback.xml @@ -2,6 +2,14 @@ + + + true + + + + ./logs/grpc/grpc.log + + ./logs/grpc/grpc-%d{yyyy-MM-dd}.%i.log.gz + 500MB + 7 + 50GB + + + %d{HH:mm:ss.SSS} %-5level [%t] [%c{1}] %m%n + + + TRACE + + + + + 1024 + 0 + true + 5000 + + + 0 @@ -61,19 +93,17 @@ 100 true + + 5000 - - - - - - - + + + @@ -88,6 +118,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -97,6 +150,8 @@ + + + - diff --git a/framework/src/test/java/org/tron/common/log/LogServiceTest.java b/framework/src/test/java/org/tron/common/log/LogServiceTest.java new file mode 100644 index 00000000000..3ac00a9e599 --- /dev/null +++ b/framework/src/test/java/org/tron/common/log/LogServiceTest.java @@ -0,0 +1,138 @@ +package org.tron.common.log; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.jul.LevelChangePropagator; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; +import org.tron.core.exception.TronError; + +/** + * Verifies that {@link LogService#load(String)} keeps the Logback<->JUL level + * bridge working even when the active configuration does not declare a + * {@code LevelChangePropagator} itself. + */ +public class LogServiceTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void restoreDefaultLogbackConfig() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + lc.reset(); + try { + new ContextInitializer(lc).autoConfig(); + } catch (JoranException e) { + Assert.fail("failed to restore default logback config: " + e.getMessage()); + } + } + + @Test + public void propagatorIsInstalledWhenCustomConfigOmitsIt() throws IOException { + Path xml = writeLogbackXml("DEBUG", false); + + LogService.load(xml.toString()); + + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + assertEquals(1, countLevelChangePropagators(lc)); + + // LevelChangePropagator maps Logback DEBUG -> JUL FINE. + Level julLevel = Logger.getLogger("io.grpc").getLevel(); + assertNotNull("JUL level for io.grpc should be synced from Logback", julLevel); + assertEquals(Level.FINE, julLevel); + } + + @Test + public void propagatorIsNotDuplicatedWhenCustomConfigDeclaresIt() throws IOException { + Path xml = writeLogbackXml("INFO", true); + + LogService.load(xml.toString()); + + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + assertEquals("XML-declared propagator should not be duplicated", + 1, countLevelChangePropagators(lc)); + } + + @Test + public void propagatorIsEnsuredWhenNoLogConfigIsSupplied() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + // Drop whatever the default logback-test.xml registered so we can observe the + // fall-through path (no --log-config) installing the propagator on its own. + removeLevelChangePropagators(lc); + assertEquals(0, countLevelChangePropagators(lc)); + + // Empty path == no --log-config passed; must keep classpath default AND + // still install the propagator so JUL sync works. + LogService.load(""); + + assertEquals("ensureLevelChangePropagator should run on the default context", + 1, countLevelChangePropagators(lc)); + } + + @Test + public void nonEmptyInvalidPathFailsFast() { + // A non-empty --log-config that cannot be read must surface loudly instead + // of silently falling back to the classpath default. + TronError thrown = assertThrows(TronError.class, + () -> LogService.load("definitely-not-a-real-path.xml")); + assertEquals(TronError.ErrCode.LOG_LOAD, thrown.getErrCode()); + } + + private Path writeLogbackXml(String level, + boolean includePropagator) throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + if (includePropagator) { + sb.append(" \n"); + sb.append(" true\n"); + sb.append(" \n"); + } + sb.append(" \n"); + sb.append(" %m%n\n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append("\n"); + Path path = temporaryFolder.newFile("logback.xml").toPath(); + Files.write(path, sb.toString().getBytes(StandardCharsets.UTF_8)); + return path; + } + + private static int countLevelChangePropagators(LoggerContext lc) { + int count = 0; + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + count++; + } + } + return count; + } + + private static void removeLevelChangePropagators(LoggerContext lc) { + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + lc.removeListener(listener); + } + } + } +} diff --git a/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java index 7fd792a958d..41e8749e1ec 100644 --- a/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java +++ b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java @@ -19,10 +19,20 @@ package org.tron.common.storage.leveldb; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; @@ -32,11 +42,13 @@ import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.rocksdb.RocksDB; +import org.slf4j.LoggerFactory; import org.tron.common.TestConstants; import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; import org.tron.common.utils.FileUtil; import org.tron.common.utils.PropUtil; +import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.StorageUtils; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; @@ -122,4 +134,59 @@ private void makeExceptionDb(String dbName) { FileUtil.saveData(dataSource.getDbPath().toString() + "/CURRENT", "...", Boolean.FALSE); } + + @Test + public void slowOpen() throws IOException { + Logger dbLogger = (Logger) LoggerFactory.getLogger("DB"); + ListAppender dbAppender = new ListAppender<>(); + dbAppender.start(); + dbLogger.addAppender(dbAppender); + try { + final File dbDir = temporaryFolder.newFolder(); + final Path dbPath = dbDir.toPath(); + final String watchdogDbName = "slow-open-db"; + + LevelDbDataSourceImpl ds = new LevelDbDataSourceImpl(); + ReflectUtils.setFieldValue(ds, "dataBaseName", watchdogDbName); + ReflectUtils.setFieldValue(ds, "parentPath", dbDir.getParent()); + long startNs = System.nanoTime() - TimeUnit.SECONDS.toNanos(61); + ds.logSlowOpen(dbPath, startNs); + + List warns = dbAppender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .collect(Collectors.toList()); + assertEquals("expected exactly one WARN event", 1, warns.size()); + ILoggingEvent warn = warns.get(0); + assertNotNull("expected one WARN from the watchdog helper", warn); + String rendered = warn.getFormattedMessage(); + assertTrue("WARN should include the Toolkit remediation hint: " + rendered, + rendered.contains("Toolkit.jar db archive -d")); + assertTrue("WARN should echo the db name: " + rendered, + rendered.contains(watchdogDbName)); + } finally { + dbAppender.stop(); + dbLogger.detachAppender(dbAppender); + } + } + + @Test + public void fastOpen() { + Logger dbLogger = (Logger) LoggerFactory.getLogger("DB"); + ListAppender dbAppender = new ListAppender<>(); + dbAppender.start(); + dbLogger.addAppender(dbAppender); + try { + String dir = Args.getInstance().getOutputDirectory() + + Args.getInstance().getStorage().getDbDirectory(); + LevelDbDataSourceImpl ds = new LevelDbDataSourceImpl(dir, "test_fast_open"); + ds.closeDB(); + long warnCount = dbAppender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .count(); + assertEquals("no WARN should fire for a fast open", 0, warnCount); + } finally { + dbAppender.stop(); + dbLogger.detachAppender(dbAppender); + } + } } diff --git a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java index 70434430262..9c2e004931e 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -4,12 +4,20 @@ import static org.tron.protos.Protocol.Transaction.Result.contractResult.PRECOMPILED_CONTRACT; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import com.google.protobuf.ByteString; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.LoggerFactory; import org.tron.common.BaseTest; import org.tron.common.TestConstants; import org.tron.common.utils.StringUtil; @@ -69,4 +77,61 @@ public void testRemoveRedundantRet() { Assert.assertEquals(1, transactionCapsule.getInstance().getRetCount()); Assert.assertEquals(SUCCESS, transactionCapsule.getInstance().getRet(0).getContractRet()); } -} \ No newline at end of file + + @Test + public void slowVerify() { + Logger capsuleLogger = (Logger) LoggerFactory.getLogger("capsule"); + Level originalLevel = capsuleLogger.getLevel(); + capsuleLogger.setLevel(Level.INFO); + ListAppender appender = new ListAppender<>(); + appender.start(); + capsuleLogger.addAppender(appender); + try { + TransactionCapsule cap = new TransactionCapsule(Transaction.newBuilder().build()); + long startNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(51); + cap.logSlowSigVerify(startNs); + + List warns = appender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .collect(Collectors.toList()); + Assert.assertEquals("expected one WARN for a slow verify", 1, warns.size()); + String rendered = warns.get(0).getFormattedMessage(); + Assert.assertTrue("WARN should mention slow verify: " + rendered, + rendered.contains("slow verify")); + Assert.assertTrue("WARN should echo the txId: " + rendered, + rendered.contains(cap.getTransactionId().toString())); + Assert.assertTrue("WARN should include sigCount: " + rendered, + rendered.contains("sigCount=")); + Assert.assertTrue("WARN should include cost in ms: " + rendered, + rendered.contains("cost=")); + Assert.assertTrue("WARN should render ms suffix: " + rendered, + rendered.contains(" ms")); + } finally { + appender.stop(); + capsuleLogger.detachAppender(appender); + capsuleLogger.setLevel(originalLevel); + } + } + + @Test + public void fastVerify() { + Logger capsuleLogger = (Logger) LoggerFactory.getLogger("capsule"); + Level originalLevel = capsuleLogger.getLevel(); + capsuleLogger.setLevel(Level.INFO); + ListAppender appender = new ListAppender<>(); + appender.start(); + capsuleLogger.addAppender(appender); + try { + TransactionCapsule cap = new TransactionCapsule(Transaction.newBuilder().build()); + cap.logSlowSigVerify(System.nanoTime()); + long warnCount = appender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .count(); + Assert.assertEquals("no WARN should fire below the threshold", 0, warnCount); + } finally { + appender.stop(); + capsuleLogger.detachAppender(appender); + capsuleLogger.setLevel(originalLevel); + } + } +} diff --git a/framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java b/framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java new file mode 100644 index 00000000000..85ade6ba7fa --- /dev/null +++ b/framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java @@ -0,0 +1,84 @@ +package org.tron.core.config; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TronLogShutdownHookTest { + + private boolean originalShutDown; + + @Before + public void saveShutDownFlag() { + originalShutDown = TronLogShutdownHook.shutDown; + } + + @After + public void restoreShutDownFlag() { + TronLogShutdownHook.shutDown = originalShutDown; + } + + @Test(timeout = 5_000) + public void returnsImmediatelyWhenAlreadyShutDown() { + TronLogShutdownHook.shutDown = true; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + long startNs = System.nanoTime(); + hook.run(); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + assertTrue("hook should exit fast when shutDown==true, elapsed=" + elapsedMs + "ms", + elapsedMs < 2_000); + } + + @Test(timeout = 10_000) + public void wakesUpWhenShutDownFlagFlips() throws InterruptedException { + TronLogShutdownHook.shutDown = false; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + Thread runner = new Thread(hook, "shutdown-hook-test-runner"); + runner.setDaemon(true); + runner.start(); + + Thread.sleep(300); + long flipNs = System.nanoTime(); + TronLogShutdownHook.shutDown = true; + + runner.join(5_000); + long elapsedAfterFlipMs = + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - flipNs); + + assertFalse("runner should have exited after flag flipped, still alive", + runner.isAlive()); + // The loop sleeps in 100 ms slices, so it should wake up well inside one + // slice's worth of jitter. 1 s is comfortable even on slow CI. + assertTrue("hook should return shortly after flag flip, elapsed=" + + elapsedAfterFlipMs + "ms", elapsedAfterFlipMs < 1_000); + } + + @Test(timeout = 10_000) + public void preservesInterruptStatusWhenInterrupted() throws InterruptedException { + TronLogShutdownHook.shutDown = false; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + AtomicBoolean interruptedAfterRun = new AtomicBoolean(false); + Thread runner = new Thread(() -> { + hook.run(); + interruptedAfterRun.set(Thread.currentThread().isInterrupted()); + }, "shutdown-hook-test-interrupt"); + runner.setDaemon(true); + runner.start(); + + Thread.sleep(200); + runner.interrupt(); + + runner.join(5_000); + assertFalse("runner should have exited after interrupt", runner.isAlive()); + assertTrue("run() must re-assert interrupt status after catching " + + "InterruptedException", interruptedAfterRun.get()); + } +} diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index 344b6dd946d..91559d86362 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -93,7 +93,14 @@ public void LogLoadTest() throws IOException { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { - LogService.load("non-existent.xml"); + // Empty path means no --log-config supplied: keep the classpath default. + LogService.load(""); + // Non-empty path pointing at a missing file must fail fast so operators + // don't silently run with the classpath default. + TronError missing = assertThrows(TronError.class, + () -> LogService.load("non-existent.xml")); + assertEquals(TronError.ErrCode.LOG_LOAD, missing.getErrCode()); + // Non-empty path pointing at an unparseable file must also surface. Path path = temporaryFolder.newFile("logback.xml").toPath(); TronError thrown = assertThrows(TronError.class, () -> LogService.load(path.toString())); assertEquals(TronError.ErrCode.LOG_LOAD, thrown.getErrCode()); diff --git a/framework/src/test/resources/logback-test.xml b/framework/src/test/resources/logback-test.xml index cc8c84e831f..54462a6761d 100644 --- a/framework/src/test/resources/logback-test.xml +++ b/framework/src/test/resources/logback-test.xml @@ -1,5 +1,9 @@ + + true + + @@ -31,12 +35,13 @@ - + + diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8ef39e7669b..983528f0002 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2245,12 +2245,20 @@ - - - + + + - - + + + + + + + + + + @@ -2279,21 +2287,6 @@ - - - - - - - - - - - - - - - diff --git a/plugins/src/main/resources/logback.xml b/plugins/src/main/resources/logback.xml index 6c415042e38..fa557f1a412 100644 --- a/plugins/src/main/resources/logback.xml +++ b/plugins/src/main/resources/logback.xml @@ -50,8 +50,10 @@ + + + -