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. *