diff --git a/build.gradle b/build.gradle
index e143ab3a94..65e72c0fb7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,6 +5,10 @@ plugins {
id 'net.ltgt.errorprone' version '5.0.0' apply false
}
+ext {
+ grpcVersion = "1.83.0"
+}
+
allprojects {
version = "1.0.0"
apply plugin: "java-library"
@@ -41,7 +45,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.81.0' : '1.60.0'
+ ProtocGenVersion: isArm64 || isMac ? rootProject.grpcVersion : '1.60.0'
],
VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions"
]
diff --git a/common/build.gradle b/common/build.gradle
index 3fc955f9ad..14d3eb4e63 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -21,7 +21,7 @@ dependencies {
api 'org.aspectj:aspectjrt:1.9.8'
api 'org.aspectj:aspectjweaver:1.9.8'
api 'org.aspectj:aspectjtools:1.9.8'
- api group: 'io.github.tronprotocol', name: 'libp2p', version: '2.2.8',{
+ api group: 'io.github.tronprotocol', name: 'libp2p', version: '2.2.9',{
exclude group: 'io.grpc', module: 'grpc-context'
exclude group: 'io.grpc', module: 'grpc-core'
exclude group: 'io.grpc', module: 'grpc-netty'
diff --git a/common/src/main/java/org/tron/core/config/README.md b/common/src/main/java/org/tron/core/config/README.md
index 1380c98984..5618cb9fed 100644
--- a/common/src/main/java/org/tron/core/config/README.md
+++ b/common/src/main/java/org/tron/core/config/README.md
@@ -54,8 +54,9 @@ node {
# Number of gRPC thread, default availableProcessors / 2
# thread = 16
- # The maximum number of concurrent calls permitted for each incoming connection
- # maxConcurrentCallsPerConnection =
+ # The maximum number of concurrent calls permitted for each incoming connection,
+ # default 100. Setting 0 also uses the secure default.
+ # maxConcurrentCallsPerConnection = 100
# The HTTP/2 flow control window, default 1MB
# flowControlWindow =
@@ -75,6 +76,10 @@ node {
}
```
+> **Upgrade note:** `maxConcurrentCallsPerConnection = 0` previously disabled the limit.
+> It now selects the secure default of 100. Configure an explicit positive value if a node
+> requires more than 100 concurrent calls on one connection.
+
## backup
You can customize backup options in the `node.backup` part of `config.conf`, which looks like:
```
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 2158f56d0b..91945b5a73 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
@@ -207,6 +207,8 @@ public static class HttpConfig {
@Setter
public static class RpcConfig {
+ public static final int DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION = 100;
+
private boolean enable = true;
private int port = 50051;
private boolean solidityEnable = true;
@@ -215,7 +217,8 @@ public static class RpcConfig {
private int pBFTPort = 50071;
private int thread = 0;
- private int maxConcurrentCallsPerConnection = 0;
+ private int maxConcurrentCallsPerConnection =
+ DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION;
private int flowControlWindow = 1048576;
private long maxConnectionIdleInMillis = 0;
private long maxConnectionAgeInMillis = 0;
@@ -358,8 +361,17 @@ private void postProcess() {
rpc.thread = (Runtime.getRuntime().availableProcessors() + 1) / 2;
}
+ if (rpc.maxConcurrentCallsPerConnection < 0) {
+ throw new TronError("node.rpc.maxConcurrentCallsPerConnection must be non-negative, got: "
+ + rpc.maxConcurrentCallsPerConnection, PARAMETER_INIT);
+ }
if (rpc.maxConcurrentCallsPerConnection == 0) {
- rpc.maxConcurrentCallsPerConnection = Integer.MAX_VALUE;
+ logger.warn("Configuring [node.rpc.maxConcurrentCallsPerConnection] as 0 no longer "
+ + "disables the limit; using the secure default of {}. Configure an explicit positive "
+ + "value if more concurrency is required.",
+ RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION);
+ rpc.maxConcurrentCallsPerConnection =
+ RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION;
}
if (rpc.maxConnectionIdleInMillis == 0) {
rpc.maxConnectionIdleInMillis = Long.MAX_VALUE;
diff --git a/common/src/main/java/org/tron/json/JSON.java b/common/src/main/java/org/tron/json/JSON.java
index 571b9515ad..ddab0af82a 100644
--- a/common/src/main/java/org/tron/json/JSON.java
+++ b/common/src/main/java/org/tron/json/JSON.java
@@ -50,6 +50,8 @@ public final class JSON {
.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true)
// Fastjson Feature.IgnoreNotMatch (default ON) — unknown fields silently ignored
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ // Fastjson 1.x rejects non-comment tokens after the root value
+ .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true)
// Fastjson serializes empty beans as "{}" without error
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
// Fastjson omits null-valued fields by default (WriteMapNullValue is OFF by default)
diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf
index 25fc4832e5..d8c483d932 100644
--- a/common/src/main/resources/reference.conf
+++ b/common/src/main/resources/reference.conf
@@ -289,9 +289,8 @@ node {
# Number of gRPC threads, 0 = auto (availableProcessors / 2)
thread = 0
- # Maximum concurrent calls per incoming connection
- # 0 means No limit on concurrent calls per connection
- maxConcurrentCallsPerConnection = 0
+ # Maximum concurrent calls per incoming connection. 0 falls back to the secure default of 100.
+ maxConcurrentCallsPerConnection = 100
# HTTP/2 flow control window (bytes), default 1MB
flowControlWindow = 1048576
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 bbc2d2475e..bcb8b09dd7 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
@@ -2,11 +2,13 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.junit.Test;
+import org.tron.core.exception.TronError;
public class NodeConfigTest {
@@ -102,7 +104,8 @@ public void testRpcDefaultsFromReference() {
NodeConfig.RpcConfig rpc = nc.getRpc();
// reference.conf provides actual final defaults, no sentinel conversion needed
- assertEquals(2147483647, rpc.getMaxConcurrentCallsPerConnection());
+ assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION,
+ rpc.getMaxConcurrentCallsPerConnection());
assertEquals(1048576, rpc.getFlowControlWindow());
assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis());
assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis());
@@ -122,6 +125,27 @@ public void testRpcUserOverrideZeroNotConverted() {
assertEquals(0, nc.getRpc().getMinEffectiveConnection());
}
+ @Test
+ public void testRpcZeroConcurrentCallsUsesSecureDefault() {
+ Config config = withRef(
+ "node { rpc { maxConcurrentCallsPerConnection = 0 } }");
+ NodeConfig nc = NodeConfig.fromConfig(config);
+ assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION,
+ nc.getRpc().getMaxConcurrentCallsPerConnection());
+ }
+
+ @Test
+ public void testRpcNegativeConcurrentCallsRejected() {
+ Config config = withRef(
+ "node { rpc { maxConcurrentCallsPerConnection = -1 } }");
+
+ TronError exception = assertThrows(TronError.class,
+ () -> NodeConfig.fromConfig(config));
+
+ assertTrue(exception.getMessage().contains(
+ "node.rpc.maxConcurrentCallsPerConnection must be non-negative, got: -1"));
+ }
+
@Test
public void testRpcUserOverrideExplicitValues() {
Config config = withRef(
diff --git a/docs/configuration.md b/docs/configuration.md
index 28b53b1970..d021326a15 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -104,8 +104,8 @@ node {
port = 50051
solidityEnable = true
solidityPort = 50061
- # Maximum concurrent calls per connection. 0 = no limit.
- maxConcurrentCallsPerConnection = 0
+ # Maximum concurrent calls per connection. 0 uses the secure default of 100.
+ maxConcurrentCallsPerConnection = 100
# Idle connection timeout (ms). 0 = no limit.
maxConnectionIdleInMillis = 0
# Minimum active connections required before broadcasting transactions.
@@ -114,6 +114,10 @@ node {
}
```
+> **Upgrade note:** `node.rpc.maxConcurrentCallsPerConnection = 0` previously meant no limit.
+> It now selects the secure default of 100. Configure an explicit positive value if a client
+> needs more than 100 concurrent calls on one connection.
+
To disable an API endpoint that you do not want to expose publicly, set its `Enable` flag to `false` or add endpoints to `node.disabledApi`:
```hocon
diff --git a/framework/build.gradle b/framework/build.gradle
index 0ce33f253c..8255fc30d1 100644
--- a/framework/build.gradle
+++ b/framework/build.gradle
@@ -40,6 +40,10 @@ dependencies {
// end local libraries
implementation group: 'com.beust', name: 'jcommander', version: '1.78'
implementation group: 'io.dropwizard.metrics', name: 'metrics-core', version: '3.1.2'
+ implementation('io.netty:netty-codec-protobuf:4.2.15.Final') {
+ exclude group: 'com.google.protobuf'
+ exclude group: 'com.google.protobuf.nano'
+ }
// http
implementation 'org.eclipse.jetty:jetty-server:9.4.58.v20250814'
implementation 'org.eclipse.jetty:jetty-servlet:9.4.58.v20250814'
diff --git a/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java b/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java
new file mode 100644
index 0000000000..cdd71ffee3
--- /dev/null
+++ b/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java
@@ -0,0 +1,79 @@
+/*
+ * java-tron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * java-tron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with java-tron. If not, see .
+ */
+
+package org.tron.common.application;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.grpc.netty.GrpcHttp2ConnectionHandler;
+import io.grpc.netty.InternalProtocolNegotiator;
+import io.grpc.netty.InternalProtocolNegotiators;
+import io.grpc.netty.NettyServerBuilder;
+import io.netty.channel.ChannelHandler;
+import io.netty.util.AsciiString;
+
+/** Enforces the advertised HTTP/2 concurrent stream limit for grpc-netty servers. */
+final class GrpcNettyMaxConcurrentStreamsLimiter {
+
+ private GrpcNettyMaxConcurrentStreamsLimiter() {
+ }
+
+ static NettyServerBuilder configurePlaintext(
+ NettyServerBuilder builder, int maxConcurrentStreams) {
+ checkNotNull(builder, "builder");
+ checkArgument(maxConcurrentStreams > 0, "maxConcurrentStreams must be positive");
+ builder.maxConcurrentCallsPerConnection(maxConcurrentStreams);
+ // TODO: Remove this shim after https://github.com/grpc/grpc-java/issues/12930 is fixed.
+ return builder.protocolNegotiator(newPlaintextNegotiator(maxConcurrentStreams));
+ }
+
+ static InternalProtocolNegotiator.ProtocolNegotiator newPlaintextNegotiator(
+ int maxConcurrentStreams) {
+ checkArgument(maxConcurrentStreams > 0, "maxConcurrentStreams must be positive");
+ return new EnforcingProtocolNegotiator(
+ InternalProtocolNegotiators.serverPlaintext(), maxConcurrentStreams);
+ }
+
+ private static final class EnforcingProtocolNegotiator
+ implements InternalProtocolNegotiator.ProtocolNegotiator {
+
+ private final InternalProtocolNegotiator.ProtocolNegotiator delegate;
+ private final int maxConcurrentStreams;
+
+ private EnforcingProtocolNegotiator(
+ InternalProtocolNegotiator.ProtocolNegotiator delegate, int maxConcurrentStreams) {
+ this.delegate = checkNotNull(delegate, "delegate");
+ this.maxConcurrentStreams = maxConcurrentStreams;
+ }
+
+ @Override
+ public AsciiString scheme() {
+ return delegate.scheme();
+ }
+
+ @Override
+ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) {
+ // grpc-java builds the connection directly, bypassing Netty's builder-side enforcement.
+ grpcHandler.connection().remote().maxActiveStreams(maxConcurrentStreams);
+ return delegate.newHandler(grpcHandler);
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+ }
+}
diff --git a/framework/src/main/java/org/tron/common/application/RpcService.java b/framework/src/main/java/org/tron/common/application/RpcService.java
index c398b71ae4..27fcc479f4 100644
--- a/framework/src/main/java/org/tron/common/application/RpcService.java
+++ b/framework/src/main/java/org/tron/common/application/RpcService.java
@@ -100,8 +100,9 @@ protected NettyServerBuilder initServerBuilder() {
serverBuilder = serverBuilder.executor(this.executorService);
}
// Set configs from config.conf or default value
+ serverBuilder = GrpcNettyMaxConcurrentStreamsLimiter.configurePlaintext(
+ serverBuilder, parameter.getMaxConcurrentCallsPerConnection());
serverBuilder
- .maxConcurrentCallsPerConnection(parameter.getMaxConcurrentCallsPerConnection())
.flowControlWindow(parameter.getFlowControlWindow())
.maxConnectionIdle(parameter.getMaxConnectionIdleInMillis(), TimeUnit.MILLISECONDS)
.maxConnectionAge(parameter.getMaxConnectionAgeInMillis(), TimeUnit.MILLISECONDS)
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 e6ccb4e4d1..2fa7d9fbb4 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
@@ -58,7 +58,6 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
import org.tron.common.utils.Commons;
import org.tron.common.utils.StringUtil;
import org.tron.core.Constant;
-import org.tron.json.JSON;
import org.tron.protos.contract.BalanceContract;
/**
@@ -851,29 +850,27 @@ static String escapeBytes(ByteString input) {
return ByteArray.toHexString(input.toByteArray());
}
- static String escapeBytes(ByteString input, final String fliedName, boolean selfType) {
+ static String escapeBytes(ByteString input, final String fieldName, boolean selfType) {
if (!selfType) {
return ByteArray.toHexString(input.toByteArray());
} else {
- return escapeBytesSelfType(input, fliedName);
+ return escapeBytesSelfType(input, fieldName);
}
}
- static String escapeBytesSelfType(ByteString input, final String fliedName) {
+ static String escapeBytesSelfType(ByteString input, final String fieldName) {
//Address
- if (HttpSelfFormatFieldName.isAddressFormat(fliedName)) {
+ if (HttpSelfFormatFieldName.isAddressFormat(fieldName)) {
return StringUtil.encode58Check(input.toByteArray());
}
//Normal String
- if (HttpSelfFormatFieldName.isNameStringFormat(fliedName)) {
- String result = new String(input.toByteArray());
- result = result.replaceAll("\"", "\\\\\"");
- try {
- JSON.parseObject("{\"key\":\"" + result + "\"}");
- return result;
- } catch (Exception e) {
+ if (HttpSelfFormatFieldName.isNameStringFormat(fieldName)) {
+ // Preserve arbitrary bytes losslessly as hex instead of decoding malformed UTF-8 with
+ // the platform default charset.
+ if (!input.isValidUtf8()) {
return ByteArray.toHexString(input.toByteArray());
}
+ return escapeNameStringText(input.toStringUtf8());
}
//HEX
return ByteArray.toHexString(input.toByteArray());
@@ -904,6 +901,29 @@ static ByteString unescapeBytes(CharSequence input) throws InvalidEscapeSequence
// Some of these methods are package-private because Descriptors.java uses
// them.
+ /**
+ * Escapes a valid UTF-8 name-string without exposing it to the U+FFFF sentinel used by
+ * {@link StringCharacterIterator}. Keeping this workaround on the new bytes path avoids
+ * changing the established behavior of {@link #escapeText(String)} for proto string fields.
+ */
+ private static String escapeNameStringText(String input) {
+ int index = input.indexOf(Character.MAX_VALUE);
+ if (index < 0) {
+ return escapeText(input);
+ }
+
+ StringBuilder result = new StringBuilder(input.length());
+ int start = 0;
+ while (index >= 0) {
+ result.append(escapeText(input.substring(start, index)));
+ result.append(Character.MAX_VALUE);
+ start = index + 1;
+ index = input.indexOf(Character.MAX_VALUE, start);
+ }
+ result.append(escapeText(input.substring(start)));
+ return result.toString();
+ }
+
/**
* Implements JSON string escaping as specified here.
*
- The following characters are escaped by prefixing them with a '\' :
@@ -1004,6 +1024,9 @@ static String unescapeText(String input) throws InvalidEscapeSequence {
case '\\':
builder.append('\\');
break;
+ case '/':
+ builder.append('/');
+ break;
case '"':
builder.append('\"');
break;
@@ -1358,7 +1381,7 @@ static ByteString unescapeBytesSelfType(String input, final String fliedName)
//Normal String -> ByteString
if (HttpSelfFormatFieldName.isNameStringFormat(fliedName)) {
- return ByteString.copyFromUtf8(input);
+ return ByteString.copyFromUtf8(unescapeText(input));
}
return unescapeBytes(input);
diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java
index bf435bf095..f34d440702 100644
--- a/framework/src/main/java/org/tron/program/Version.java
+++ b/framework/src/main/java/org/tron/program/Version.java
@@ -2,9 +2,9 @@
public class Version {
- public static final String VERSION_NAME = "GreatVoyage-v4.8.1.1-173-gaced0d5654";
- public static final String VERSION_CODE = "18817";
- private static final String VERSION = "4.8.2";
+ public static final String VERSION_NAME = "GreatVoyage-v4.8.2-6-g348db25bfd";
+ public static final String VERSION_CODE = "18825";
+ private static final String VERSION = "4.8.2.1";
public static String getVersion() {
return VERSION;
diff --git a/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java b/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java
new file mode 100644
index 0000000000..fc578ca794
--- /dev/null
+++ b/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java
@@ -0,0 +1,108 @@
+/*
+ * java-tron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * java-tron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with java-tron. If not, see .
+ */
+
+package org.tron.common.application;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import io.grpc.ChannelLogger;
+import io.grpc.ChannelLogger.ChannelLogLevel;
+import io.grpc.netty.GrpcHttp2ConnectionHandler;
+import io.grpc.netty.InternalProtocolNegotiator;
+import io.netty.channel.ChannelHandler;
+import io.netty.handler.codec.http2.DefaultHttp2Connection;
+import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
+import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
+import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
+import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
+import io.netty.handler.codec.http2.Http2Connection;
+import io.netty.handler.codec.http2.Http2ConnectionDecoder;
+import io.netty.handler.codec.http2.Http2ConnectionEncoder;
+import io.netty.handler.codec.http2.Http2Error;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.handler.codec.http2.Http2FrameWriter;
+import io.netty.handler.codec.http2.Http2Settings;
+import org.junit.Test;
+
+public class GrpcNettyMaxConcurrentStreamsLimiterTest {
+
+ private static final ChannelLogger NOOP_LOGGER = new ChannelLogger() {
+ @Override
+ public void log(ChannelLogLevel level, String message) {
+ }
+
+ @Override
+ public void log(ChannelLogLevel level, String messageFormat, Object... args) {
+ }
+ };
+
+ @Test
+ public void shouldEnforceMaxStreamsBeforeSettingsAck() throws Exception {
+ Http2Connection connection = new DefaultHttp2Connection(true);
+ GrpcHttp2ConnectionHandler grpcHandler = newGrpcHandler(connection);
+ InternalProtocolNegotiator.ProtocolNegotiator negotiator =
+ GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(2);
+
+ ChannelHandler negotiationHandler = negotiator.newHandler(grpcHandler);
+
+ assertNotNull(negotiationHandler);
+ assertEquals(2, connection.remote().maxActiveStreams());
+ connection.remote().createStream(1, true);
+ connection.remote().createStream(3, true);
+ Http2Exception exception = assertThrows(
+ Http2Exception.class, () -> connection.remote().createStream(5, true));
+ assertEquals(Http2Error.REFUSED_STREAM, exception.error());
+ negotiator.close();
+ }
+
+ @Test
+ public void shouldIgnoreClientMaxHeaderListSizeOnServer() throws Exception {
+ Http2Connection connection = new DefaultHttp2Connection(true);
+ Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter();
+ Http2ConnectionEncoder encoder =
+ new DefaultHttp2ConnectionEncoder(connection, frameWriter);
+ long originalMaxHeaderListSize =
+ encoder.configuration().headersConfiguration().maxHeaderListSize();
+
+ encoder.remoteSettings(new Http2Settings().maxHeaderListSize(1));
+
+ assertEquals(originalMaxHeaderListSize,
+ encoder.configuration().headersConfiguration().maxHeaderListSize());
+ encoder.close();
+ }
+
+ @Test
+ public void shouldRejectNonPositiveStreamLimit() {
+ IllegalArgumentException zeroLimitException = assertThrows(IllegalArgumentException.class,
+ () -> GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(0));
+ assertEquals("maxConcurrentStreams must be positive", zeroLimitException.getMessage());
+ IllegalArgumentException negativeLimitException = assertThrows(IllegalArgumentException.class,
+ () -> GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(-1));
+ assertEquals("maxConcurrentStreams must be positive", negativeLimitException.getMessage());
+ }
+
+ private static GrpcHttp2ConnectionHandler newGrpcHandler(Http2Connection connection) {
+ Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter();
+ Http2ConnectionEncoder encoder =
+ new DefaultHttp2ConnectionEncoder(connection, frameWriter);
+ Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(
+ connection, encoder, new DefaultHttp2FrameReader());
+ return new GrpcHttp2ConnectionHandler(
+ null, decoder, encoder, new Http2Settings(), NOOP_LOGGER) {
+ };
+ }
+}
diff --git a/framework/src/test/java/org/tron/common/application/RpcServiceHttp2SecurityTest.java b/framework/src/test/java/org/tron/common/application/RpcServiceHttp2SecurityTest.java
new file mode 100644
index 0000000000..9ad83cccec
--- /dev/null
+++ b/framework/src/test/java/org/tron/common/application/RpcServiceHttp2SecurityTest.java
@@ -0,0 +1,283 @@
+/*
+ * java-tron is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * java-tron is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with java-tron. If not, see .
+ */
+
+package org.tron.common.application;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.tron.common.math.StrictMathWrapper.min;
+
+import com.google.protobuf.Empty;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Server;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerServiceDefinition;
+import io.grpc.netty.NettyServerBuilder;
+import io.grpc.protobuf.ProtoUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
+import io.netty.handler.codec.http2.Http2Error;
+import io.netty.handler.codec.http2.Http2Headers;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.tron.common.parameter.CommonParameter;
+import org.tron.core.config.args.Args;
+
+public class RpcServiceHttp2SecurityTest {
+
+ private static final int HEADERS_FRAME_TYPE = 0x1;
+ private static final int RST_STREAM_FRAME_TYPE = 0x3;
+ private static final int SETTINGS_FRAME_TYPE = 0x4;
+ private static final int GO_AWAY_FRAME_TYPE = 0x7;
+ private static final int SETTINGS_MAX_CONCURRENT_STREAMS = 0x3;
+ private static final byte[] CLIENT_PREFACE =
+ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] EMPTY_SETTINGS_FRAME =
+ new byte[]{0, 0, 0, 4, 0, 0, 0, 0, 0};
+ private static final String SERVICE_NAME = "test.HoldService";
+ private static final String METHOD_NAME = "Hold";
+ private static final String METHOD_PATH = "/" + SERVICE_NAME + "/" + METHOD_NAME;
+
+ private CommonParameter parameter;
+ private int previousRpcThreadNum;
+ private int previousMaxConcurrentCalls;
+ private int previousFlowControlWindow;
+ private long previousMaxConnectionIdle;
+ private long previousMaxConnectionAge;
+ private int previousMaxMessageSize;
+ private int previousMaxHeaderListSize;
+ private int previousMaxRstStream;
+ private int previousSecondsPerWindow;
+ private boolean previousReflectionServiceEnable;
+
+ @Before
+ public void setUp() {
+ parameter = Args.getInstance();
+ previousRpcThreadNum = parameter.getRpcThreadNum();
+ previousMaxConcurrentCalls = parameter.getMaxConcurrentCallsPerConnection();
+ previousFlowControlWindow = parameter.getFlowControlWindow();
+ previousMaxConnectionIdle = parameter.getMaxConnectionIdleInMillis();
+ previousMaxConnectionAge = parameter.getMaxConnectionAgeInMillis();
+ previousMaxMessageSize = parameter.getMaxMessageSize();
+ previousMaxHeaderListSize = parameter.getMaxHeaderListSize();
+ previousMaxRstStream = parameter.getRpcMaxRstStream();
+ previousSecondsPerWindow = parameter.getRpcSecondsPerWindow();
+ previousReflectionServiceEnable = parameter.isRpcReflectionServiceEnable();
+
+ parameter.setRpcThreadNum(0);
+ parameter.setMaxConcurrentCallsPerConnection(2);
+ parameter.setFlowControlWindow(NettyServerBuilder.DEFAULT_FLOW_CONTROL_WINDOW);
+ parameter.setMaxConnectionIdleInMillis(60_000);
+ parameter.setMaxConnectionAgeInMillis(Long.MAX_VALUE);
+ parameter.setMaxMessageSize(4 * 1024 * 1024);
+ parameter.setMaxHeaderListSize(8 * 1024);
+ parameter.setRpcMaxRstStream(0);
+ parameter.setRpcSecondsPerWindow(0);
+ parameter.setRpcReflectionServiceEnable(false);
+ }
+
+ @After
+ public void tearDown() {
+ parameter.setRpcThreadNum(previousRpcThreadNum);
+ parameter.setMaxConcurrentCallsPerConnection(previousMaxConcurrentCalls);
+ parameter.setFlowControlWindow(previousFlowControlWindow);
+ parameter.setMaxConnectionIdleInMillis(previousMaxConnectionIdle);
+ parameter.setMaxConnectionAgeInMillis(previousMaxConnectionAge);
+ parameter.setMaxMessageSize(previousMaxMessageSize);
+ parameter.setMaxHeaderListSize(previousMaxHeaderListSize);
+ parameter.setRpcMaxRstStream(previousMaxRstStream);
+ parameter.setRpcSecondsPerWindow(previousSecondsPerWindow);
+ parameter.setRpcReflectionServiceEnable(previousReflectionServiceEnable);
+ }
+
+ @Test
+ public void shouldRejectExcessStreamsBeforeClientAcknowledgesSettings() throws Exception {
+ TestRpcService rpcService = new TestRpcService();
+ Server server = rpcService.newServerBuilder()
+ .addService(newHoldService())
+ .build()
+ .start();
+
+ try (Socket socket = new Socket("127.0.0.1", server.getPort())) {
+ socket.setSoTimeout(5_000);
+ OutputStream output = socket.getOutputStream();
+ output.write(CLIENT_PREFACE);
+ output.write(EMPTY_SETTINGS_FRAME);
+ output.write(newHeadersFrame(1));
+ output.write(newHeadersFrame(3));
+ output.write(newHeadersFrame(5));
+ output.flush();
+
+ assertSettingsAndRefusedStream(socket.getInputStream(), 2, 5);
+ } finally {
+ server.shutdownNow();
+ assertTrue(server.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ private static ServerServiceDefinition newHoldService() {
+ MethodDescriptor method =
+ MethodDescriptor.newBuilder()
+ .setType(MethodDescriptor.MethodType.BIDI_STREAMING)
+ .setFullMethodName(MethodDescriptor.generateFullMethodName(
+ SERVICE_NAME, METHOD_NAME))
+ .setRequestMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
+ .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
+ .build();
+
+ return ServerServiceDefinition.builder(SERVICE_NAME)
+ .addMethod(method, new ServerCallHandler() {
+ @Override
+ public ServerCall.Listener startCall(
+ ServerCall call, Metadata headers) {
+ return new ServerCall.Listener() {
+ };
+ }
+ })
+ .build();
+ }
+
+ private static byte[] newHeadersFrame(int streamId) throws Exception {
+ DefaultHttp2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder();
+ ByteBuf headerBlock = Unpooled.buffer();
+ ByteBuf frame = Unpooled.buffer();
+ try {
+ Http2Headers headers = new DefaultHttp2Headers()
+ .method("POST")
+ .scheme("http")
+ .authority("localhost")
+ .path(METHOD_PATH)
+ .set("content-type", "application/grpc")
+ .set("te", "trailers");
+ encoder.encodeHeaders(streamId, headers, headerBlock);
+
+ frame.writeMedium(headerBlock.readableBytes());
+ frame.writeByte(HEADERS_FRAME_TYPE);
+ frame.writeByte(0x4);
+ frame.writeInt(streamId);
+ frame.writeBytes(headerBlock);
+ return ByteBufUtil.getBytes(frame);
+ } finally {
+ frame.release();
+ headerBlock.release();
+ encoder.close();
+ }
+ }
+
+ private static void assertSettingsAndRefusedStream(
+ InputStream input, long expectedMaxConcurrentStreams, int expectedRefusedStreamId)
+ throws IOException {
+ boolean advertisedLimitFound = false;
+ for (int i = 0; i < 20; i++) {
+ Http2Frame frame = readFrame(input);
+ if (frame.type == SETTINGS_FRAME_TYPE && frame.streamId == 0) {
+ advertisedLimitFound |= hasSetting(
+ frame.payload, SETTINGS_MAX_CONCURRENT_STREAMS, expectedMaxConcurrentStreams);
+ }
+ if (frame.type == RST_STREAM_FRAME_TYPE && frame.streamId == expectedRefusedStreamId) {
+ assertTrue("Server did not advertise the enforced concurrent-stream limit",
+ advertisedLimitFound);
+ assertEquals(4, frame.payload.length);
+ long errorCode = ByteBuffer.wrap(frame.payload).getInt() & 0xffff_ffffL;
+ assertEquals(Http2Error.REFUSED_STREAM.code(), errorCode);
+ return;
+ }
+ if (frame.type == GO_AWAY_FRAME_TYPE) {
+ fail("Server closed the connection instead of refusing only the excess stream");
+ }
+ }
+ fail("No REFUSED_STREAM response for stream " + expectedRefusedStreamId);
+ }
+
+ private static boolean hasSetting(byte[] payload, int expectedId, long expectedValue) {
+ assertEquals("Invalid HTTP/2 SETTINGS payload length", 0, payload.length % 6);
+ ByteBuffer settings = ByteBuffer.wrap(payload);
+ while (settings.remaining() >= 6) {
+ int id = settings.getShort() & 0xffff;
+ long value = settings.getInt() & 0xffff_ffffL;
+ if (id == expectedId) {
+ assertEquals(expectedValue, value);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Http2Frame readFrame(InputStream input) throws IOException {
+ byte[] header = readFully(input, 9);
+ int payloadLength =
+ ((header[0] & 0xff) << 16) | ((header[1] & 0xff) << 8) | (header[2] & 0xff);
+ int type = header[3] & 0xff;
+ int streamId = ByteBuffer.wrap(header, 5, 4).getInt() & 0x7fff_ffff;
+ return new Http2Frame(type, streamId, readFully(input, payloadLength));
+ }
+
+ private static byte[] readFully(InputStream input, int length) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream(length);
+ byte[] buffer = new byte[min(length, 1024)];
+ while (output.size() < length) {
+ int read = input.read(buffer, 0, min(buffer.length, length - output.size()));
+ if (read < 0) {
+ throw new EOFException("Unexpected end of HTTP/2 frame");
+ }
+ output.write(buffer, 0, read);
+ }
+ return output.toByteArray();
+ }
+
+ private static final class Http2Frame {
+
+ private final int type;
+ private final int streamId;
+ private final byte[] payload;
+
+ private Http2Frame(int type, int streamId, byte[] payload) {
+ this.type = type;
+ this.streamId = streamId;
+ this.payload = payload;
+ }
+ }
+
+ private static final class TestRpcService extends RpcService {
+
+ private TestRpcService() {
+ port = 0;
+ }
+
+ private NettyServerBuilder newServerBuilder() {
+ return initServerBuilder();
+ }
+
+ @Override
+ protected void addService(NettyServerBuilder serverBuilder) {
+ }
+ }
+}
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 076a8ab538..36b8a3269c 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
@@ -103,7 +103,7 @@ public void get() {
// gRPC network configs checking
Assert.assertEquals(50051, parameter.getRpcPort());
- Assert.assertEquals(Integer.MAX_VALUE, parameter.getMaxConcurrentCallsPerConnection());
+ Assert.assertEquals(100, parameter.getMaxConcurrentCallsPerConnection());
Assert
.assertEquals(NettyServerBuilder
.DEFAULT_FLOW_CONTROL_WINDOW, parameter.getFlowControlWindow());
diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatEscapeTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatEscapeTest.java
new file mode 100644
index 0000000000..a5c74cc434
--- /dev/null
+++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatEscapeTest.java
@@ -0,0 +1,698 @@
+package org.tron.core.services.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.fasterxml.jackson.core.json.JsonReadFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import java.nio.charset.StandardCharsets;
+import org.junit.Test;
+import org.tron.common.utils.ByteArray;
+import org.tron.core.capsule.TransactionCapsule;
+import org.tron.protos.Protocol.Transaction;
+import org.tron.protos.Protocol.Transaction.Contract.ContractType;
+import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract;
+
+/**
+ * Escaping and decoding of name-string {@code bytes} fields.
+ *
+ *
Before this fix {@code escapeBytesSelfType} only escaped the double quote, leaving backslash
+ * and control chars raw, and validated the result with a lenient parser. That let attacker
+ * controlled on-chain bytes emit invalid JSON and, worse, forge sibling fields on the
+ * re-parse path used by {@link Util#printTransactionToJSON}.
+ *
+ *
Assertions here always state which parser they use:
+ *
+ * - {@link #strict()} - RFC 8259 baseline. Trailing tokens are rejected. A bare
+ * {@code readTree} is NOT a strict baseline: it stops after the first value and would
+ * silently accept trailing payloads and duplicated output.
+ * - {@link org.tron.json.JSON} - the node's own lenient parser, used by
+ * {@code Util.printTransactionToJSON}. Never used to assert validity, only to reproduce
+ * what the node itself would hand back to a client.
+ *
+ */
+public class JsonFormatEscapeTest {
+
+ private static final String URL_FIELD = "protocol.AssetIssueContract.url";
+ private static final String DESC_FIELD = "protocol.AssetIssueContract.description";
+ private static final String NAME_FIELD = "protocol.AssetIssueContract.name";
+ private static final String ABBR_FIELD = "protocol.AssetIssueContract.abbr";
+ private static final String CONTRACT_NAME_FIELD =
+ "protocol.Transaction.Contract.ContractName";
+ private static final String[] NAME_STRING_FIELDS = {
+ "protocol.Return.message",
+ "protocol.Address.host",
+ "protocol.Note.memo",
+ "protocol.AccountUpdateContract.account_name",
+ "protocol.SetAccountIdContract.account_id",
+ "protocol.TransferAssetContract.asset_name",
+ "protocol.WitnessCreateContract.url",
+ "protocol.WitnessUpdateContract.update_url",
+ "protocol.AssetIssueContract.name",
+ "protocol.AssetIssueContract.abbr",
+ "protocol.AssetIssueContract.description",
+ "protocol.AssetIssueContract.url",
+ "protocol.ParticipateAssetIssueContract.asset_name",
+ "protocol.UpdateAssetContract.url",
+ "protocol.UpdateAssetContract.description",
+ "protocol.ExchangeCreateContract.first_token_id",
+ "protocol.ExchangeCreateContract.second_token_id",
+ "protocol.ExchangeInjectContract.token_id",
+ "protocol.ExchangeWithdrawContract.token_id",
+ "protocol.ExchangeTransactionContract.token_id",
+ "protocol.AccountId.name",
+ "protocol.Exchange.first_token_id",
+ "protocol.Exchange.second_token_id",
+ "protocol.Account.account_name",
+ "protocol.Account.asset_issued_name",
+ "protocol.Account.asset_issued_ID",
+ "protocol.Account.account_id",
+ "protocol.authority.permission_name",
+ "protocol.Transaction.Contract.ContractName",
+ "protocol.TransactionInfo.resMessage",
+ "protocol.MarketSellAssetContract.sell_token_id",
+ "protocol.MarketSellAssetContract.buy_token_id",
+ "protocol.MarketOrder.sell_token_id",
+ "protocol.MarketOrder.buy_token_id",
+ "protocol.MarketOrderPair.sell_token_id",
+ "protocol.MarketOrderPair.buy_token_id",
+ "protocol.MarketPriceList.sell_token_id",
+ "protocol.MarketPriceList.buy_token_id"
+ };
+
+ private static final byte[] OWNER = new byte[21];
+
+ static {
+ OWNER[0] = 0x41;
+ }
+
+ /**
+ * Strict RFC 8259 baseline for the features this issue turns on: structural leniency and
+ * unescaped control chars. Each is disabled explicitly rather than relying on Jackson's
+ * defaults, so a future default change cannot silently weaken these assertions. Number
+ * related leniency that {@link org.tron.json.JSON} also enables is not relevant here and is
+ * left at Jackson's (already strict) default.
+ */
+ private static ObjectMapper strict() {
+ return JsonMapper.builder()
+ .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
+ .disable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
+ .disable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
+ .disable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES)
+ .disable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
+ .disable(JsonReadFeature.ALLOW_TRAILING_COMMA)
+ .disable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS)
+ .build();
+ }
+
+ /** Guards the guard: the baseline must actually reject what it claims to reject. */
+ @Test
+ public void testStrictBaselineIsActuallyStrict() {
+ assertTrue(parsesStrictly("{\"a\":1}"));
+ assertFalse("trailing token", parsesStrictly("{\"a\":1} garbage"));
+ assertFalse("duplicated object (print() amplification shape)",
+ parsesStrictly("{\"a\":1}\n{\"a\":2}"));
+ assertFalse("java comment", parsesStrictly("{\"a\":1}//x"));
+ assertFalse("single quotes", parsesStrictly("{'a':1}"));
+ assertFalse("unquoted field name", parsesStrictly("{a:1}"));
+ assertFalse("raw control char in string", parsesStrictly("{\"a\":\"x\ny\"}"));
+ }
+
+ private static String escapeName(byte[] raw, String field) {
+ return JsonFormat.escapeBytesSelfType(ByteString.copyFrom(raw), field);
+ }
+
+ private static String escapeName(String raw, String field) {
+ return escapeName(raw.getBytes(StandardCharsets.UTF_8), field);
+ }
+
+ private static boolean parsesStrictly(String body) {
+ try {
+ strict().readTree(body);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /** Renders one asset issue contract exactly as the visible=true HTTP path would. */
+ private static String printContract(AssetIssueContract contract) {
+ return JsonFormat.printToString(contract, true);
+ }
+
+ private static AssetIssueContract assetWithDescription(byte[] description) {
+ return AssetIssueContract.newBuilder()
+ .setOwnerAddress(ByteString.copyFrom(OWNER))
+ .setName(ByteString.copyFromUtf8("REALTOKEN"))
+ .setTotalSupply(1000)
+ .setDescription(ByteString.copyFrom(description))
+ .setUrl(ByteString.copyFromUtf8("https://honest.example"))
+ .setFreeAssetNetLimit(111)
+ .setPublicFreeAssetNetLimit(222)
+ .build();
+ }
+
+ // Direct name-string byte escaping
+
+ @Test
+ public void testAllControlCharsAreEscaped() {
+ for (int c = 0x00; c <= 0x1F; c++) {
+ byte[] raw = new byte[] {'a', (byte) c, 'b'};
+ String escaped = escapeName(raw, URL_FIELD);
+
+ assertFalse("raw control char 0x" + Integer.toHexString(c) + " leaked into the output",
+ escaped.chars().anyMatch(ch -> ch <= 0x1F));
+ assertTrue("strict parser rejected escaped control char 0x" + Integer.toHexString(c),
+ parsesStrictly("{\"url\":\"" + escaped + "\"}"));
+ }
+ }
+
+ @Test
+ public void testBackslashIsEscaped() {
+ assertEquals("C:\\\\dir", escapeName("C:\\dir", URL_FIELD));
+ assertTrue(parsesStrictly("{\"url\":\"" + escapeName("C:\\dir", URL_FIELD) + "\"}"));
+ }
+
+ @Test
+ public void testDoubleQuoteIsEscaped() {
+ assertEquals("a\\\"b", escapeName("a\"b", URL_FIELD));
+ assertTrue(parsesStrictly("{\"url\":\"" + escapeName("a\"b", URL_FIELD) + "\"}"));
+ }
+
+ @Test
+ public void testNewlineIsEscapedNotRaw() {
+ assertEquals("a\\nb", escapeName("a\nb", URL_FIELD));
+ }
+
+ @Test
+ public void testValidUtf8IsPreserved() {
+ assertEquals("ok", escapeName("ok", URL_FIELD));
+ assertEquals("中文", escapeName("中文", URL_FIELD));
+ }
+
+ @Test
+ public void testInvalidUtf8FallsBackToHexWithoutReplacementChar() {
+ byte[] raw = new byte[] {0x61, (byte) 0xff, 0x62};
+ String escaped = escapeName(raw, URL_FIELD);
+
+ assertEquals("61ff62", escaped);
+ assertFalse("invalid UTF-8 must not be decoded into U+FFFD mojibake",
+ escaped.indexOf('�') >= 0);
+ assertTrue(parsesStrictly("{\"url\":\"" + escaped + "\"}"));
+ }
+
+ @Test
+ public void testUnpairedSurrogateBytesFallBackToHexInsteadOfThrowing() {
+ // 0xED 0xA0 0x80 is the UTF-8 style encoding of an unpaired high surrogate. escapeText()
+ // would throw on it, so isValidUtf8() must divert it to hex first.
+ byte[] raw = new byte[] {(byte) 0xed, (byte) 0xa0, (byte) 0x80};
+
+ assertEquals("eda080", escapeName(raw, URL_FIELD));
+ }
+
+ @Test
+ public void testSupplementaryPlaneIsEmittedAsSurrogateEscapes() {
+ String emoji = new String(Character.toChars(0x1F600));
+
+ assertEquals("\\ud83d\\ude00", escapeName(emoji, URL_FIELD));
+ }
+
+ // Field integrity through HTTP normalization
+
+ /**
+ * Reproduces {@code Util.printTransactionToJSON} line 272:
+ * {@code JSONObject.parseObject(JsonFormat.printToString(contract, selfType))}.
+ */
+ private static org.tron.json.JSONObject reparseAsNodeDoes(AssetIssueContract contract) {
+ return org.tron.json.JSONObject.parseObject(printContract(contract));
+ }
+
+ /**
+ * The node does not stop at parseObject: it re-serializes with toJSONString() and returns
+ * that. Pre-fix, that step laundered a forged object into well formed JSON the client could
+ * not tell apart from a genuine response. Assert the whole chain, not just the parse.
+ */
+ private static void assertNoForgeryThroughNormalization(AssetIssueContract contract)
+ throws Exception {
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+ String normalized = parsed.toJSONString();
+
+ assertTrue("normalized output must be valid JSON: " + normalized,
+ parsesStrictly(normalized));
+
+ // Assert on parsed FIELD VALUES, not on substring presence: the attacker payload is stored
+ // in description, so the literal text "evil"/"TFAKE" legitimately appears there. What must
+ // not happen is those values landing in url / name / owner_address.
+ JsonNode node = strict().readTree(normalized);
+ assertEquals("url must be the real one", "https://honest.example",
+ node.get("url").asText());
+ assertEquals("name must be the real one", "REALTOKEN", node.get("name").asText());
+ assertTrue("owner_address must be the real base58 owner",
+ node.get("owner_address").asText().startsWith("T"));
+ assertFalse("owner_address must not be attacker supplied",
+ node.get("owner_address").asText().contains("TFAKE"));
+ assertEquals("total_supply must be the real one", 1000, node.get("total_supply").asLong());
+ }
+
+ @Test
+ public void testSingleQuotePayloadCannotForgeSiblingField() throws Exception {
+ // ALLOW_SINGLE_QUOTES is enabled on the node's lenient parser, so a payload can supply a
+ // value without ever using a double quote, which the old replaceAll() would have escaped.
+ byte[] payload = "a\\\",url:'https://evil'}//".getBytes(StandardCharsets.UTF_8);
+ AssetIssueContract contract = assetWithDescription(payload);
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ assertEquals(new String(payload, StandardCharsets.UTF_8),
+ String.valueOf(parsed.get("description")));
+ assertTrue("strict parser must accept the emitted document",
+ parsesStrictly(printContract(contract)));
+ assertNoForgeryThroughNormalization(contract);
+ }
+
+ @Test
+ public void testDuplicateKeyPayloadCannotOverrideEarlierFields() throws Exception {
+ byte[] payload =
+ "x\\\",owner_address:'TFAKE',name:'FAKE',total_supply:1,url:'https://evil'}//"
+ .getBytes(StandardCharsets.UTF_8);
+ AssetIssueContract contract = assetWithDescription(payload);
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertEquals("REALTOKEN", String.valueOf(parsed.get("name")));
+ assertEquals(1000, Integer.parseInt(String.valueOf(parsed.get("total_supply"))));
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ assertFalse("owner_address must not be attacker supplied",
+ String.valueOf(parsed.get("owner_address")).contains("TFAKE"));
+ assertNoForgeryThroughNormalization(contract);
+ }
+
+ @Test
+ public void testUnquotedFieldNamePayloadInjectsNothing() {
+ AssetIssueContract contract =
+ assetWithDescription("a\\\",b:1}//".getBytes(StandardCharsets.UTF_8));
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertNull("no attacker key may appear", parsed.get("b"));
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ }
+
+ @Test
+ public void testBlockCommentPayloadInjectsNothing() {
+ AssetIssueContract contract =
+ assetWithDescription("a\\\",b:1}/*".getBytes(StandardCharsets.UTF_8));
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertNull(parsed.get("b"));
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ }
+
+ @Test
+ public void testTrailingTokenPayloadInjectsNothing() {
+ AssetIssueContract contract =
+ assetWithDescription("a\\\"}".getBytes(StandardCharsets.UTF_8));
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ assertTrue(parsesStrictly(printContract(contract)));
+ }
+
+ // Preservation of fields serialized after attacker-controlled values
+
+ @Test
+ public void testFieldsAfterPayloadSurvive() {
+ AssetIssueContract contract =
+ assetWithDescription("a\\\"}//".getBytes(StandardCharsets.UTF_8));
+
+ org.tron.json.JSONObject parsed = reparseAsNodeDoes(contract);
+
+ assertEquals("https://honest.example", String.valueOf(parsed.get("url")));
+ assertEquals(111, Integer.parseInt(String.valueOf(parsed.get("free_asset_net_limit"))));
+ assertEquals(222,
+ Integer.parseInt(String.valueOf(parsed.get("public_free_asset_net_limit"))));
+ }
+
+ @Test
+ public void testAssetNameEscapingPreservesFollowingFields() {
+ byte[] value = "a\\\"}//".getBytes(StandardCharsets.UTF_8);
+ assertEquals("a\\\\\\\"}//", escapeName(value, NAME_FIELD));
+
+ AssetIssueContract contract = AssetIssueContract.newBuilder()
+ .setOwnerAddress(ByteString.copyFrom(OWNER))
+ .setName(ByteString.copyFrom(value))
+ .setUrl(ByteString.copyFromUtf8("https://honest.example"))
+ .build();
+
+ assertTrue(parsesStrictly(printContract(contract)));
+ assertEquals("https://honest.example",
+ String.valueOf(reparseAsNodeDoes(contract).get("url")));
+ }
+
+ @Test
+ public void testAssetAbbrEscapingPreservesFollowingFields() {
+ byte[] value = "\\\"}".getBytes(StandardCharsets.UTF_8);
+ assertEquals("\\\\\\\"}", escapeName(value, ABBR_FIELD));
+
+ AssetIssueContract contract = AssetIssueContract.newBuilder()
+ .setOwnerAddress(ByteString.copyFrom(OWNER))
+ .setAbbr(ByteString.copyFrom(value))
+ .setUrl(ByteString.copyFromUtf8("https://honest.example"))
+ .build();
+
+ assertTrue(parsesStrictly(printContract(contract)));
+ assertEquals("https://honest.example",
+ String.valueOf(reparseAsNodeDoes(contract).get("url")));
+ }
+
+ @Test
+ public void testAllNameStringByteFieldsUseEscapeText() {
+ String value = "a\\nb";
+ String escaped = JsonFormat.escapeText(value);
+
+ assertEquals("a\\\\nb", escaped);
+ for (String field : NAME_STRING_FIELDS) {
+ assertTrue(field, HttpSelfFormatFieldName.isNameStringFormat(field));
+ assertEquals(field, escaped, escapeName(value, field));
+ }
+ }
+
+ @Test
+ public void testContractNameRetainsVisibleNameStringMapping() throws Exception {
+ String value = "contract-name";
+ Transaction.Contract.Builder actualContract = Transaction.Contract.newBuilder();
+ JsonFormat.merge("{\"ContractName\":\"" + value + "\"}", actualContract, true);
+ Transaction.Contract expectedContract = Transaction.Contract.newBuilder()
+ .setContractName(ByteString.copyFromUtf8(value))
+ .build();
+ Transaction actual = Transaction.newBuilder()
+ .setRawData(Transaction.raw.newBuilder().addContract(actualContract))
+ .build();
+ Transaction expected = Transaction.newBuilder()
+ .setRawData(Transaction.raw.newBuilder().addContract(expectedContract))
+ .build();
+
+ assertEquals(value, escapeName(value, CONTRACT_NAME_FIELD));
+ assertEquals(expectedContract.toByteString(), actualContract.build().toByteString());
+ assertEquals(expected.getRawData().toByteString(), actual.getRawData().toByteString());
+ assertEquals(new TransactionCapsule(expected).getTransactionId(),
+ new TransactionCapsule(actual).getTransactionId());
+ }
+
+ // Inbound decoding and transaction ID derivation
+
+ private static AssetIssueContract mergeDescription(String rawJsonToken) throws Exception {
+ AssetIssueContract.Builder builder = AssetIssueContract.newBuilder();
+ JsonFormat.merge("{\"description\":\"" + rawJsonToken + "\"}", builder, true);
+ return builder.build();
+ }
+
+ private static Transaction transactionWithAsset(AssetIssueContract asset) {
+ Transaction.Contract contract = Transaction.Contract.newBuilder()
+ .setType(ContractType.AssetIssueContract)
+ .setParameter(Any.pack(asset))
+ .build();
+ return Transaction.newBuilder()
+ .setRawData(Transaction.raw.newBuilder()
+ .setTimestamp(1_234_567_890L)
+ .addContract(contract))
+ .build();
+ }
+
+ private static void assertDecodedInboundMapping(String rawJsonToken, String expectedValue)
+ throws Exception {
+ AssetIssueContract actualAsset = mergeDescription(rawJsonToken);
+ AssetIssueContract expectedAsset = AssetIssueContract.newBuilder()
+ .setDescription(ByteString.copyFromUtf8(expectedValue))
+ .build();
+ Transaction actual = transactionWithAsset(actualAsset);
+ Transaction expected = transactionWithAsset(expectedAsset);
+
+ assertEquals("visible=true name-string bytes must decode JSON escape sequences",
+ expectedAsset.toByteString(), actualAsset.toByteString());
+ assertEquals("decoded name-string bytes must determine raw_data",
+ expected.getRawData().toByteString(), actual.getRawData().toByteString());
+ assertEquals("the transaction ID must be derived from the decoded raw_data",
+ new TransactionCapsule(expected).getTransactionId(),
+ new TransactionCapsule(actual).getTransactionId());
+ }
+
+ @Test
+ public void testInboundNameStringEscapesDetermineRawDataAndTxId() throws Exception {
+ assertDecodedInboundMapping("plain", "plain");
+ assertDecodedInboundMapping("C:\\\\dir", "C:\\dir");
+ assertDecodedInboundMapping("a\\nb", "a\nb");
+ assertDecodedInboundMapping("a\\\"b", "a\"b");
+ assertDecodedInboundMapping("a\\u0041", "aA");
+ }
+
+ @Test
+ public void testInboundNameStringHandlesEscapesAndRejectsMalformedUnicode() throws Exception {
+ assertDecodedInboundMapping("https:\\/\\/tron.network", "https://tron.network");
+ assertDecodedInboundMapping("a\\u+123", "a" + (char) 0x0123);
+ org.junit.Assert.assertThrows(
+ "non-hex unicode escapes must still be rejected",
+ JsonFormat.ParseException.class,
+ () -> mergeDescription("a\\uZZZZ"));
+ }
+
+ @Test
+ public void testValidUtf8NameStringsRoundTripThroughEscaping() throws Exception {
+ for (String value : new String[] {
+ "plain",
+ "C:\\dir",
+ "a\nb",
+ "a\"b",
+ "a\\u0041",
+ "中文",
+ "\uFFFF sentinel",
+ new String(Character.toChars(0x1F600))}) {
+ ByteString original = ByteString.copyFromUtf8(value);
+ String escaped = JsonFormat.escapeBytesSelfType(original, DESC_FIELD);
+ ByteString decoded = JsonFormat.Tokenizer.unescapeBytesSelfType(escaped, DESC_FIELD);
+
+ assertEquals("valid UTF-8 name-string bytes must survive escape and unescape",
+ original, decoded);
+ }
+ }
+
+ @Test
+ public void testOutboundSerializationDoesNotChangeStoredRawDataOrReportedTxId() {
+ AssetIssueContract asset = AssetIssueContract.newBuilder()
+ .setDescription(ByteString.copyFromUtf8("C:\\dir\n\"quoted\""))
+ .build();
+ Transaction transaction = transactionWithAsset(asset);
+ ByteString rawDataBefore = transaction.getRawData().toByteString();
+ String txIdBefore = ByteArray.toHexString(
+ new TransactionCapsule(transaction).getTransactionId().getBytes());
+
+ org.tron.json.JSONObject output = Util.printTransactionToJSON(transaction, true);
+
+ assertEquals("query serialization must not modify the protobuf loaded from storage",
+ rawDataBefore, transaction.getRawData().toByteString());
+ assertEquals("the reported txID must still be derived from the stored raw_data bytes",
+ txIdBefore, output.getString("txID"));
+ }
+
+ @Test
+ public void testInvalidUtf8IsHexAndNotPromisedToRoundTrip() {
+ byte[] raw = new byte[] {0x61, (byte) 0xff, 0x62};
+ String escaped = escapeName(raw, URL_FIELD);
+
+ assertEquals(ByteArray.toHexString(raw), escaped);
+ assertTrue(parsesStrictly("{\"url\":\"" + escaped + "\"}"));
+ assertFalse("hex text is not expected to reconstruct the original bytes",
+ ByteString.copyFromUtf8(escaped).equals(ByteString.copyFrom(raw)));
+ }
+
+ // U+FFFF handling on the selected name-string bytes path
+
+ @Test
+ public void testUffffDoesNotTruncateNameStringField() {
+ // EF BF BF is valid UTF-8, so it must remain text without reaching escapeText's sentinel.
+ String value = "SAFE\uFFFF TAIL";
+ assertTrue(ByteString.copyFromUtf8(value).isValidUtf8());
+
+ assertEquals(value, escapeName(value, URL_FIELD));
+ }
+
+ @Test
+ public void testUffffLeadingCharDoesNotEmptyTheValue() {
+ assertEquals("\uFFFFwhole value", escapeName("\uFFFFwhole value", URL_FIELD));
+ }
+
+ // Proto string inbound compatibility
+
+ private static String mergeId(String rawJsonToken, boolean visible) throws Exception {
+ AssetIssueContract.Builder builder = AssetIssueContract.newBuilder();
+ JsonFormat.merge("{\"id\":\"" + rawJsonToken + "\"}", builder, visible);
+ return builder.getId();
+ }
+
+ @Test
+ public void testProtoStringInboundEscapes() throws Exception {
+ for (boolean visible : new boolean[] {true, false}) {
+ assertEquals("escaped solidus must be accepted as valid JSON",
+ "a/b", mergeId("a\\/b", visible));
+ assertEquals("signed unicode escape must retain the develop-branch mapping",
+ "a" + (char) 0x0123, mergeId("a\\u+123", visible));
+ assertEquals("standard unicode escape must remain unchanged",
+ "aA", mergeId("a\\u0041", visible));
+ }
+ }
+
+ @Test
+ public void testNewlineHeavyValueDoesNotAmplifyOutput() {
+ byte[] url = new byte[256];
+ java.util.Arrays.fill(url, (byte) 'A');
+ for (int i = 0; i < 128; i++) {
+ url[i] = '\n';
+ }
+ AssetIssueContract contract = AssetIssueContract.newBuilder()
+ .setOwnerAddress(ByteString.copyFrom(OWNER))
+ .setUrl(ByteString.copyFrom(url))
+ .build();
+
+ String body = printContract(contract);
+
+ assertTrue("output must not blow up relative to the input", body.length() < 1024);
+ assertTrue(parsesStrictly(body));
+ }
+
+ // Deterministic property sweep
+
+ // The fixed seed and bounds give every reviewer the same inputs. For each input:
+ // P1: escaping never throws.
+ // P2: the emitted document is strictly valid.
+ // P3: fields serialized after the payload retain their values under both the strict parser
+ // and the node's lenient parser; no key is truncated, forged, or dropped.
+ // P4: serialization does not modify the source protobuf bytes.
+ // P5: valid UTF-8 name-string bytes survive outbound escaping and inbound decoding.
+
+ @Test
+ public void testPropertySweep() throws Exception {
+ int checked = 0;
+ // every single byte, and every byte framed by ASCII
+ for (int i = 0; i < 256; i++) {
+ checked += sweepOne(new byte[] {(byte) i});
+ checked += sweepOne(new byte[] {'a', (byte) i, 'b'});
+ }
+ // every 2-byte combination: covers all malformed UTF-8 lead/continuation pairs
+ for (int i = 0; i < 256; i++) {
+ for (int j = 0; j < 256; j++) {
+ checked += sweepOne(new byte[] {(byte) i, (byte) j});
+ }
+ }
+ // structural payloads built from the characters that matter to a JSON parser
+ byte[] nasty = "\\\"'{}[],:/*\n\r\tu0 ".getBytes(StandardCharsets.UTF_8);
+ java.util.Random rnd = new java.util.Random(20260720L);
+ for (int n = 0; n < 20000; n++) {
+ byte[] buf = new byte[1 + rnd.nextInt(16)];
+ for (int i = 0; i < buf.length; i++) {
+ buf[i] = rnd.nextInt(2) == 0
+ ? nasty[rnd.nextInt(nasty.length)] : (byte) rnd.nextInt(256);
+ }
+ checked += sweepOne(buf);
+ }
+ // notable code points, including the U+FFFF sentinel and both surrogate halves
+ for (int cp : new int[] {0x00, 0x1F, 0x20, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFD, 0xFFFE,
+ 0xFFFF, 0x10000, 0x1F600, 0x10FFFF}) {
+ checked += sweepOne(new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8));
+ }
+
+ assertTrue("sweep must actually run", checked > 65000);
+ }
+
+ /** Returns 1 so the caller can count coverage. Throws AssertionError on any violation. */
+ private static int sweepOne(byte[] raw) throws Exception {
+ AssetIssueContract original = AssetIssueContract.newBuilder()
+ .setOwnerAddress(ByteString.copyFrom(OWNER))
+ .setDescription(ByteString.copyFrom(raw))
+ .setUrl(ByteString.copyFromUtf8("https://sentinel.example"))
+ .setFreeAssetNetLimit(4242)
+ .build();
+
+ ByteString originalBytes = original.toByteString();
+ String json = printContract(original); // P1
+ String where = " for input " + ByteArray.toHexString(raw);
+ assertEquals("P4 source protobuf changed during serialization" + where,
+ originalBytes, original.toByteString()); // P4
+
+ JsonNode strictNode;
+ try {
+ strictNode = strict().readTree(json); // P2
+ } catch (Exception e) {
+ throw new AssertionError("P2 strict parse failed" + where + ": " + json, e);
+ }
+ assertEquals("P3 strict: url" + where,
+ "https://sentinel.example", strictNode.get("url").asText());
+ assertEquals("P3 strict: free_asset_net_limit" + where,
+ 4242, strictNode.get("free_asset_net_limit").asLong());
+
+ org.tron.json.JSONObject lenient = org.tron.json.JSONObject.parseObject(json);
+ assertEquals("P3 lenient: url" + where,
+ "https://sentinel.example", String.valueOf(lenient.get("url")));
+ assertEquals("P3 lenient: free_asset_net_limit" + where,
+ "4242", String.valueOf(lenient.get("free_asset_net_limit")));
+
+ ByteString rawBytes = ByteString.copyFrom(raw);
+ if (rawBytes.isValidUtf8()) {
+ assertEquals("P5 valid UTF-8 must round-trip" + where, rawBytes,
+ JsonFormat.Tokenizer.unescapeBytesSelfType(
+ escapeName(raw, DESC_FIELD), DESC_FIELD));
+ } else {
+ assertEquals("invalid UTF-8 must be emitted as plain hex" + where,
+ ByteArray.toHexString(raw), escapeName(raw, DESC_FIELD));
+ }
+ return 1;
+ }
+
+ // Unaffected-format regressions
+
+ @Test
+ public void testSafeAsciiIsUnchanged() {
+ // "Safe ASCII" means no backslash, no double quote and no control chars. Plain ASCII as a
+ // whole is not unchanged, because those three classes are exactly what now gets escaped.
+ for (String safe : new String[] {
+ "https://tron.network",
+ "TronToken",
+ "abc-123_456.789~xyz",
+ ""}) {
+ assertEquals(safe, escapeName(safe, URL_FIELD));
+ }
+ }
+
+ @Test
+ public void testAddressFieldsAreUnaffected() {
+ String encoded = JsonFormat.escapeBytesSelfType(ByteString.copyFrom(OWNER),
+ "protocol.AssetIssueContract.owner_address");
+
+ assertTrue("address fields must still be base58check", encoded.startsWith("T"));
+ }
+
+ @Test
+ public void testNonNameStringBytesFieldStillHex() {
+ byte[] raw = "a\nb".getBytes(StandardCharsets.UTF_8);
+
+ assertEquals(ByteArray.toHexString(raw),
+ JsonFormat.escapeBytesSelfType(ByteString.copyFrom(raw), "protocol.Some.unmapped_field"));
+ }
+
+ @Test
+ public void testVisibleFalseStillHex() {
+ byte[] raw = "a\nb".getBytes(StandardCharsets.UTF_8);
+
+ assertEquals(ByteArray.toHexString(raw),
+ JsonFormat.escapeBytes(ByteString.copyFrom(raw), DESC_FIELD, false));
+ }
+}
diff --git a/framework/src/test/java/org/tron/json/JsonTest.java b/framework/src/test/java/org/tron/json/JsonTest.java
index f430188611..081e51d557 100644
--- a/framework/src/test/java/org/tron/json/JsonTest.java
+++ b/framework/src/test/java/org/tron/json/JsonTest.java
@@ -94,6 +94,23 @@ public void testComment() {
obj = JSON.parseObject("{/* comment */\"a\":1}");
assertNotNull(obj);
assertEquals(1, obj.getIntValue("a"));
+ obj = JSON.parseObject("{\"a\":1} /* trailing comment */");
+ assertNotNull(obj);
+ assertEquals(1, obj.getIntValue("a"));
+ }
+
+ @Test
+ public void testTrailingNonCommentTokensRejected() {
+ assertThrows(JSONException.class,
+ () -> JSON.parseObject("{\"a\":1} {\"b\":2}"));
+ assertThrows(JSONException.class,
+ () -> JSONObject.parseObject("{\"a\":1} [2]"));
+ assertThrows(JSONException.class,
+ () -> JSON.parse("{\"a\":1} garbage"));
+ assertThrows(JSONException.class,
+ () -> JSON.parseArray("[1] [2]"));
+ assertThrows(JSONException.class,
+ () -> JSONArray.parseArray("[1] true"));
}
diff --git a/gradle/java-tron.vmoptions b/gradle/java-tron.vmoptions
index e994a33274..cf34689ebd 100644
--- a/gradle/java-tron.vmoptions
+++ b/gradle/java-tron.vmoptions
@@ -4,4 +4,5 @@
-XX:+PrintGCDateStamps
-XX:+CMSParallelRemarkEnabled
-XX:ReservedCodeCacheSize=256m
--XX:+CMSScavengeBeforeRemark
\ No newline at end of file
+-XX:+CMSScavengeBeforeRemark
+-Dio.netty.allocator.type=pooled
diff --git a/gradle/jdk17/java-tron.vmoptions b/gradle/jdk17/java-tron.vmoptions
index 7af3123d26..180ff1aa7c 100644
--- a/gradle/jdk17/java-tron.vmoptions
+++ b/gradle/jdk17/java-tron.vmoptions
@@ -5,4 +5,5 @@
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=1g
--XX:+HeapDumpOnOutOfMemoryError
\ No newline at end of file
+-XX:+HeapDumpOnOutOfMemoryError
+-Dio.netty.allocator.type=pooled
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 832d2728f0..6a3e641d5d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -432,6 +432,14 @@
+
+
+
+
+
+
+
+
@@ -445,6 +453,11 @@
+
+
+
+
+
@@ -490,6 +503,14 @@
+
+
+
+
+
+
+
+
@@ -526,6 +547,11 @@
+
+
+
+
+
@@ -635,6 +661,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -680,6 +717,11 @@
+
+
+
+
+
@@ -730,6 +772,11 @@
+
+
+
+
+
@@ -754,6 +801,14 @@
+
+
+
+
+
+
+
+
@@ -762,6 +817,14 @@
+
+
+
+
+
+
+
+
@@ -772,6 +835,11 @@
+
+
+
+
+
@@ -1084,15 +1152,15 @@
-
-
-
+
+
+
-
-
+
+
-
-
+
+
@@ -1103,76 +1171,76 @@
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
@@ -1183,111 +1251,127 @@
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
@@ -1877,6 +1961,14 @@
+
+
+
+
+
+
+
+
diff --git a/protocol/build.gradle b/protocol/build.gradle
index 0ce01a9bfb..ed8914343b 100644
--- a/protocol/build.gradle
+++ b/protocol/build.gradle
@@ -2,8 +2,6 @@ apply plugin: 'com.google.protobuf'
apply from: 'protoLint.gradle'
def protobufVersion = '3.25.8'
-// 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
@@ -12,11 +10,11 @@ dependencies {
// checkstyleConfig "com.puppycrawl.tools:checkstyle:${versions.checkstyle}"
// google grpc
- api group: 'io.grpc', name: 'grpc-netty', version: grpcVersion
- api group: 'io.grpc', name: 'grpc-protobuf', version: grpcVersion
- api group: 'io.grpc', name: 'grpc-stub', version: grpcVersion
- api group: 'io.grpc', name: 'grpc-core', version: grpcVersion
- api group: 'io.grpc', name: 'grpc-services', version: grpcVersion
+ api group: 'io.grpc', name: 'grpc-netty', version: rootProject.grpcVersion
+ api group: 'io.grpc', name: 'grpc-protobuf', version: rootProject.grpcVersion
+ api group: 'io.grpc', name: 'grpc-stub', version: rootProject.grpcVersion
+ api group: 'io.grpc', name: 'grpc-core', version: rootProject.grpcVersion
+ api group: 'io.grpc', name: 'grpc-services', version: rootProject.grpcVersion
// end google grpc
diff --git a/start.sh b/start.sh
index 89f13cf25a..1472a94dc6 100644
--- a/start.sh
+++ b/start.sh
@@ -358,7 +358,8 @@ startService() {
nohup $JAVACMD -Xms$JVM_MS -Xmx$JVM_MX -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:./gc.log \
-XX:+PrintGCDateStamps -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m -XX:+UseCodeCacheFlushing \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
- -XX:MaxDirectMemorySize=$MAX_DIRECT_MEMORY -XX:+HeapDumpOnOutOfMemoryError \
+ -XX:MaxDirectMemorySize=$MAX_DIRECT_MEMORY -Dio.netty.allocator.type=pooled \
+ -XX:+HeapDumpOnOutOfMemoryError \
-XX:NewRatio=2 -jar \
$JAR_NAME $FULL_START_OPT >>start.log 2>&1 &
checkPid
diff --git a/start.sh.simple b/start.sh.simple
index 52548dea62..109f0dc85a 100644
--- a/start.sh.simple
+++ b/start.sh.simple
@@ -137,6 +137,7 @@ startService() {
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:MaxDirectMemorySize=1g \
+ -Dio.netty.allocator.type=pooled \
-XX:+HeapDumpOnOutOfMemoryError \
-jar "$FULL_NODE_JAR" "${FULL_START_OPT[@]}" \
>> start.log 2>&1 &