From 4036f03f87c2d73ae6849138b95bae0825746f2a Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Sun, 10 May 2026 16:09:36 +0800 Subject: [PATCH 1/7] fix(config): fix the issue where --es fails to start (#6757) * fix(config): remove redundant JSON-RPC config fields and consolidate parameter binding Remove maxBlockFilterNum, maxAddressSize, and maxRequestTimeout from NodeConfig/CommonParameter since they were superseded by the jsonRpcMaxBatchSize/jsonRpcMaxResponseSize parameters. Consolidate the config binding path so all JSON-RPC size limits flow through a single reference.conf entry, and clean up stale test-config fixtures. * fix bug of NodeConfigTest * remove allowShieldedTransactionApi from reference.conf * add testcase of external.ip is null * change comment * fix the bug of --es and 7 failed testcases --- .../java/org/tron/core/config/args/Args.java | 40 +++++++++++-------- .../tron/core/net/peer/PeerConnection.java | 3 +- .../org/tron/core/config/args/ArgsTest.java | 18 +++++++++ .../ChainInventoryMsgHandlerTest.java | 14 +++++++ 4 files changed, 57 insertions(+), 18 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 2d6660f9a6a..7cc1c5b870b 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 @@ -167,16 +167,19 @@ public static void setParam(final String[] args, final String confFileName) { ? cmd.shellConfFileName : confFileName; Config config = Configuration.getByFileName(configFilePath); - // 2. Config overrides defaults + // 2. Config overrides defaults (event config bean is read here but not yet applied) applyConfigParams(config); - // 3. CLI overrides Config (highest priority) + // 3. CLI overrides Config (highest priority, including --es → eventSubscribe) applyCLIParams(cmd, jc); - // 4. Apply platform constraints (e.g. ARM64 forces RocksDB) + // 4. Apply event config after CLI + applyEventConfig(eventConfig); + + // 5. Apply platform constraints (e.g. ARM64 forces RocksDB) applyPlatformConstraints(); - // 5. Init witness (depends on CLI witness flag) + // 6. Init witness (depends on CLI witness flag) initLocalWitnesses(config, cmd); } @@ -217,7 +220,7 @@ private static void applyStorageConfig(StorageConfig sc) { 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 + // contractParse is set in applyConfigParams alongside event config, not here PARAMETER.storage.setCheckpointVersion(sc.getCheckpoint().getVersion()); PARAMETER.storage.setCheckpointSync(sc.getCheckpoint().isSync()); @@ -343,21 +346,21 @@ private static void applyRateLimiterConfig(RateLimiterConfig rl) { PARAMETER.rateLimiterInitialization = initialization; } + /** + * Package-private entry point only for tests + */ + static void applyEventConfig() { + applyEventConfig(eventConfig); + } + /** * 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()); - - // PARAMETER.eventPluginConfig and PARAMETER.eventFilter are only consumed by - // Manager.startEventSubscribing(), which itself is gated by isEventSubscribe() - // (= ec.isEnable()) at Manager.java:564. When subscribe is disabled, building - // these objects has no observable effect — skip both early so PARAMETER stays - // consistent with the runtime intent. - if (!ec.isEnable()) { + // cmd parameter has higher priority + PARAMETER.eventSubscribe = PARAMETER.eventSubscribe || ec.isEnable(); + if (!PARAMETER.eventSubscribe) { return; } @@ -770,9 +773,12 @@ public static void applyConfigParams( // node.shutdown — handled in applyNodeConfig - // Event config: bind from config.conf "event.subscribe" section + // Event config: read bean here; applyEventConfig() is called once in setParam() + // after applyCLIParams() so that --es is already reflected in eventSubscribe. eventConfig = EventConfig.fromConfig(config); - applyEventConfig(eventConfig); + // contractParse is event-domain but must be set from config before CLI can + // override it with --contract-parse-enable (which runs in applyCLIParams). + PARAMETER.storage.setContractParseSwitch(eventConfig.isContractParse()); logConfig(); } diff --git a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java index 8d7818d1608..7d7457cf2fc 100644 --- a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java +++ b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java @@ -170,7 +170,8 @@ public class PeerConnection { public void setChannel(Channel channel) { this.channel = channel; - if (relayNodes.stream().anyMatch(n -> n.getAddress().equals(channel.getInetAddress()))) { + if (relayNodes != null + && relayNodes.stream().anyMatch(n -> n.getAddress().equals(channel.getInetAddress()))) { this.isRelayPeer = true; } this.nodeStatistics = TronStatsManager.getNodeStatistics(channel.getInetAddress()); 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 4b6b7ad0a7a..3ae5677fbda 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 @@ -344,6 +344,21 @@ public void testCliEsOverridesConfig() { Args.clearParam(); } + /** + * Regression: when --es is the sole source of event.subscribe.enable=true + * (config has it disabled), eventPluginConfig must be built. + * Previously applyEventConfig() ran before applyCLIParams() and returned + * early (both flags false), leaving eventPluginConfig=null; Manager then + * called EventPluginLoader.start(null) and threw "Failed to load eventPlugin." + */ + @Test + public void testCliEsBuildsEventPluginConfig() { + Args.setParam(new String[] {"--es"}, TestConstants.TEST_CONF); + Assert.assertTrue(Args.getInstance().isEventSubscribe()); + Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); + Args.clearParam(); + } + /** * Verify that config file storage values are applied when no CLI override is present. * @@ -454,6 +469,7 @@ public void testEventConfigDisabledSkipsEpcAndFilter() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); Assert.assertNull(Args.getInstance().getEventPluginConfig()); Assert.assertNull(Args.getInstance().getEventFilter()); Args.clearParam(); @@ -467,6 +483,7 @@ public void testEventConfigEnabledBuildsEpcAndFilter() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); Assert.assertNotNull(Args.getInstance().getEventFilter()); Args.clearParam(); @@ -481,6 +498,7 @@ public void testEventConfigEnabledWithInvalidFromBlockLeavesFilterNull() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); // epc still built; filter rejected Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); Assert.assertNull(Args.getInstance().getEventFilter()); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java index dab76cfcb46..56853c3dbb7 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java @@ -3,11 +3,15 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +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.utils.Pair; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.Parameter.NetConstants; +import org.tron.core.config.args.Args; import org.tron.core.exception.P2pException; import org.tron.core.net.message.keepalive.PingMessage; import org.tron.core.net.message.sync.ChainInventoryMessage; @@ -15,6 +19,16 @@ public class ChainInventoryMsgHandlerTest { + @BeforeClass + public static void init() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + private ChainInventoryMsgHandler handler = new ChainInventoryMsgHandler(); private PeerConnection peer = new PeerConnection(); private ChainInventoryMessage msg = new ChainInventoryMessage(new ArrayList<>(), 0L); From 0a26d23ef31380f29c20abb36487b2d27674f528 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Sun, 10 May 2026 20:46:03 +0800 Subject: [PATCH 2/7] restore useNativeQueue to false default (#6759) --- common/src/main/java/org/tron/core/config/args/EventConfig.java | 2 +- common/src/main/resources/reference.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index ac1731de2dc..43707956845 100644 --- a/common/src/main/java/org/tron/core/config/args/EventConfig.java +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -43,7 +43,7 @@ public class EventConfig { @Getter @Setter public static class NativeConfig { - private boolean useNativeQueue = true; + private boolean useNativeQueue = false; private int bindport = 5555; private int sendqueuelength = 1000; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 688e1590788..1e68da051e0 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -758,7 +758,7 @@ event.subscribe = { enable = false native = { - useNativeQueue = true + useNativeQueue = false bindport = 5555 sendqueuelength = 1000 } From 9d28fa79ebcd445e22f14b600587a1caba5ace77 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Mon, 11 May 2026 16:33:00 +0800 Subject: [PATCH 3/7] fix(event): reject incompatible event-plugin versions below 3.0.0 (#6760) Pre-3.0.0(The previous event-plugin public release is 2.2.0) event-plugin builds still link against com.alibaba.fastjson, which was removed from java-tron in #6701. When such a plugin is loaded, the NoClassDefFoundError surfaces inside the plugin's own worker thread and is swallowed by its catch(Throwable) handler. The node keeps running while silently dropping every trigger, leaving operators with no signal that the event subscription is broken. Enforce a minimum Plugin-Version at startup in EventPluginLoader.startPlugin using pf4j's VersionManager (semver). When the descriptor version is below 3.0.0, log a clear upgrade hint and return false; the existing call chain in Manager.startEventSubscribing wraps that into TronError(EVENT_SUBSCRIBE_INIT) and aborts node startup instead of silently degrading. --- .../common/logsfilter/EventPluginLoader.java | 32 +++++++++++++++++++ .../common/logsfilter/EventLoaderTest.java | 31 ++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java index 7061b2e9d57..c0b7afd6779 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java +++ b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java @@ -3,6 +3,7 @@ import com.beust.jcommander.internal.Sets; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; import java.io.File; import java.util.HashSet; import java.util.List; @@ -14,8 +15,10 @@ import org.bouncycastle.util.encoders.Hex; import org.pf4j.CompoundPluginDescriptorFinder; import org.pf4j.DefaultPluginManager; +import org.pf4j.DefaultVersionManager; import org.pf4j.ManifestPluginDescriptorFinder; import org.pf4j.PluginManager; +import org.pf4j.VersionManager; import org.springframework.util.StringUtils; import org.tron.common.logsfilter.nativequeue.NativeMessageQueue; import org.tron.common.logsfilter.trigger.BlockLogTrigger; @@ -29,6 +32,16 @@ @Slf4j public class EventPluginLoader { + /** + * Minimum event-plugin Plugin-Version compatible with this node. Bumped to 3.0.0 to + * reject pre-fastjson-removal builds whose worker threads would fail with + * NoClassDefFoundError on com.alibaba.fastjson at runtime. The previous event-plugin + * release is 2.2.0, so 3.0.0 is the first version that ships the Jackson replacement. + */ + static final String MIN_PLUGIN_VERSION = "3.0.0"; + + private static final VersionManager VERSION_MANAGER = new DefaultVersionManager(); + private static EventPluginLoader instance; private long MAX_PENDING_SIZE = 50000; @@ -457,6 +470,10 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return false; } + if (!isPluginVersionSupported(pluginManager, pluginId)) { + return false; + } + pluginManager.startPlugins(); eventListeners = pluginManager.getExtensions(IPluginEventListener.class); @@ -471,6 +488,21 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return true; } + static boolean isPluginVersionSupported(PluginManager pm, String pluginId) { + String pluginVersion = pm.getPlugin(pluginId).getDescriptor().getVersion(); + if (Strings.isNullOrEmpty(pluginVersion)) { + return false; + } + boolean isSupported = VERSION_MANAGER.compareVersions(pluginVersion, MIN_PLUGIN_VERSION) >= 0; + + if (!isSupported) { + logger.error( + "event-plugin '{}' version {} is older than required {}, please upgrade event-plugin", + pluginId, pluginVersion, MIN_PLUGIN_VERSION); + } + return isSupported; + } + public void stopPlugin() { if (Objects.nonNull(pluginManager)) { pluginManager.stopPlugins(); diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java index 1e5268ddeb6..958af4f7b7b 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java @@ -3,11 +3,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import org.junit.Assert; import org.junit.Test; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; import org.tron.common.logsfilter.trigger.BlockLogTrigger; import org.tron.common.logsfilter.trigger.TransactionLogTrigger; @@ -48,6 +53,32 @@ public void launchNativeQueue() { EventPluginLoader.getInstance().stopPlugin(); } + @Test + public void testIsPluginVersionSupported() { + assertEquals("3.0.0", EventPluginLoader.MIN_PLUGIN_VERSION); + // last releases before fastjson removal — must be rejected + assertFalse(checkVersion("1.0.0")); + assertFalse(checkVersion("2.2.0")); + assertFalse(checkVersion("2.9.9")); + // 3.0.0 onward — must be accepted + assertTrue(checkVersion("3.0.0")); + assertTrue(checkVersion("3.1.5")); + assertTrue(checkVersion("10.0.0")); + // empty/null version — reject + assertFalse(checkVersion("")); + assertFalse(checkVersion(null)); + } + + private static boolean checkVersion(String version) { + PluginManager pm = mock(PluginManager.class); + PluginWrapper wrapper = mock(PluginWrapper.class); + PluginDescriptor desc = mock(PluginDescriptor.class); + when(pm.getPlugin("test")).thenReturn(wrapper); + when(wrapper.getDescriptor()).thenReturn(desc); + when(desc.getVersion()).thenReturn(version); + return EventPluginLoader.isPluginVersionSupported(pm, "test"); + } + @Test public void testBlockLogTrigger() { BlockLogTrigger blt = new BlockLogTrigger(); From 5f7eeca0771536bccdd706fa2612454e8ae5eba8 Mon Sep 17 00:00:00 2001 From: xxo1_shine Date: Tue, 12 May 2026 16:38:41 +0800 Subject: [PATCH 4/7] feat(ratelimiter): add configurable blocking mode for API rate limiters (#6761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `rate.limiter.apiNonBlocking` switch (default false). When off, callers wait for a permit; when on, they reject immediately and shed load. - Wire the switch through `RateLimiterConfig` → `Args` → `CommonParameter`; update `reference.conf` / `config.conf`. - Add blocking `acquire()` paths on `IRateLimiter`, `GlobalRateLimiter`, and `QpsStrategy` / `IPQpsStrategy` / `GlobalPreemptibleStrategy`. Only the semaphore-based strategy is bounded (2s); the QPS-based paths use unbounded Guava `RateLimiter.acquire()`. - Introduce `acquirePermit()` dispatcher (default method on `IRateLimiter`; static on `GlobalRateLimiter`) that picks blocking vs non-blocking based on the switch. - Swap `tryAcquire` → `acquirePermit` at the `RateLimiterServlet` and `RateLimiterInterceptor` call sites; preserve per-endpoint-before-global ordering to avoid consuming global quota on per-endpoint rejection. - Extract `loadIpLimiter()` in `GlobalRateLimiter` and `loadLimiter()` in `IPQpsStrategy` to share cache+exception handling between `tryAcquire` and `acquire`. - Add unit tests covering dispatcher routing (both directions), acquire IP-before-global ordering, and IP-loader-failure fail-closed behaviour. --- .../common/parameter/CommonParameter.java | 3 + .../core/config/args/RateLimiterConfig.java | 1 + common/src/main/resources/reference.conf | 1 + .../config/args/RateLimiterConfigTest.java | 6 +- .../java/org/tron/core/config/args/Args.java | 1 + .../services/http/RateLimiterServlet.java | 7 +- .../ratelimiter/GlobalRateLimiter.java | 42 +++++-- .../ratelimiter/RateLimiterInterceptor.java | 7 +- .../adapter/DefaultBaseQqsAdapter.java | 5 + .../adapter/GlobalPreemptibleAdapter.java | 4 + .../adapter/IPQPSRateLimiterAdapter.java | 5 + .../ratelimiter/adapter/IRateLimiter.java | 8 ++ .../adapter/QpsRateLimiterAdapter.java | 5 + .../strategy/GlobalPreemptibleStrategy.java | 24 +++- .../ratelimiter/strategy/IPQpsStrategy.java | 20 +++- .../ratelimiter/strategy/QpsStrategy.java | 5 + framework/src/main/resources/config.conf | 5 +- .../services/http/RateLimiterServletTest.java | 22 ++-- .../ratelimiter/GlobalRateLimiterTest.java | 106 ++++++++++++++++++ .../RateLimiterInterceptorTest.java | 28 ++--- .../ratelimiter/adaptor/AdaptorTest.java | 61 ++++++++++ 21 files changed, 314 insertions(+), 52 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 3fe1e878ffb..8bbc52e6d87 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -411,6 +411,9 @@ public class CommonParameter { @Setter public double rateLimiterDisconnect; // clearParam: 1.0 @Getter + @Setter + public boolean rateLimiterApiNonBlocking = false; + @Getter public RocksDbSettings rocksDBCustomSettings; @Getter public GenesisBlock genesisBlock; 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 index eed5ef1898b..5eab6f6d92d 100644 --- a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -21,6 +21,7 @@ public class RateLimiterConfig { private P2pRateLimitConfig p2p = new P2pRateLimitConfig(); private List http = new ArrayList<>(); private List rpc = new ArrayList<>(); + private boolean apiNonBlocking = false; @Getter @Setter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 1e68da051e0..3f41c556f96 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -451,6 +451,7 @@ rate.limiter = { global.qps = 50000 global.ip.qps = 10000 global.api.qps = 1000 + apiNonBlocking = false } seed.node = { 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 index 7b4d8a87d45..c3b827a8ba4 100644 --- a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java @@ -1,6 +1,7 @@ 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; @@ -29,6 +30,7 @@ public void testDefaults() { assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001); assertTrue(rl.getHttp().isEmpty()); assertTrue(rl.getRpc().isEmpty()); + assertFalse(rl.isApiNonBlocking()); } @Test @@ -40,7 +42,8 @@ public void testFromConfig() { + " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter," + " paramString = \"qps=10\" }]," + " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter," - + " paramString = \"permit=1\" }]" + + " paramString = \"permit=1\" }]," + + " apiNonBlocking = true" + "}"); RateLimiterConfig rl = RateLimiterConfig.fromConfig(config); assertEquals(100, rl.getGlobal().getQps()); @@ -50,5 +53,6 @@ public void testFromConfig() { assertEquals("TestServlet", rl.getHttp().get(0).getComponent()); assertEquals(1, rl.getRpc().size()); assertEquals("TestRpc", rl.getRpc().get(0).getComponent()); + assertTrue(rl.isApiNonBlocking()); } } 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 7cc1c5b870b..ec808267c75 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 @@ -328,6 +328,7 @@ private static void applyRateLimiterConfig(RateLimiterConfig rl) { PARAMETER.rateLimiterSyncBlockChain = rl.getP2p().getSyncBlockChain(); PARAMETER.rateLimiterFetchInvData = rl.getP2p().getFetchInvData(); PARAMETER.rateLimiterDisconnect = rl.getP2p().getDisconnect(); + PARAMETER.rateLimiterApiNonBlocking = rl.isApiNonBlocking(); // HTTP/RPC rate limiter items: convert bean lists to business objects RateLimiterInitialization initialization = new RateLimiterInitialization(); 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 3086cbb3619..f488c32df4c 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 @@ -107,9 +107,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) IRateLimiter rateLimiter = container.get(KEY_PREFIX_HTTP, getClass().getSimpleName()); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java index 4b3043274d2..11c55e3a2c3 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java @@ -23,21 +23,43 @@ public class GlobalRateLimiter { public static boolean tryAcquire(RuntimeData runtimeData) { String ip = runtimeData.getRemoteAddr(); if (!Strings.isNullOrEmpty(ip)) { - RateLimiter r; - try { - // cache.get is atomic: only one loader executes per key under concurrent requests, - // preventing multiple RateLimiter instances from being created for the same IP. - r = cache.get(ip, () -> RateLimiter.create(IP_QPS)); - } catch (Exception e) { - logger.warn("Failed to load IP rate limiter for {}, denying request: {}", - ip, e.getMessage()); + RateLimiter r = loadIpLimiter(ip); + if (r == null || !r.tryAcquire()) { return false; } - if (!r.tryAcquire()) { + } + return rateLimiter.tryAcquire(); + } + + public static boolean acquire(RuntimeData runtimeData) { + String ip = runtimeData.getRemoteAddr(); + if (!Strings.isNullOrEmpty(ip)) { + RateLimiter r = loadIpLimiter(ip); + if (r == null) { return false; } + r.acquire(); + } + rateLimiter.acquire(); + return true; + } + + public static boolean acquirePermit(RuntimeData runtimeData) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(runtimeData) + : acquire(runtimeData); + } + + private static RateLimiter loadIpLimiter(String ip) { + try { + // cache.get is atomic: only one loader executes per key under concurrent requests, + // preventing multiple RateLimiter instances from being created for the same IP. + return cache.get(ip, () -> RateLimiter.create(IP_QPS)); + } catch (Exception e) { + logger.warn("Failed to load IP rate limiter for {}, denying request: {}", + ip, e.getMessage()); + return null; } - return rateLimiter.tryAcquire(); } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java index a07cf955828..85e94f2e768 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java @@ -108,9 +108,10 @@ public Listener interceptCall(ServerCall call, RuntimeData runtimeData = new RuntimeData(call); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); if (!acquireResource) { // Release the per-endpoint permit when global rejected, to avoid semaphore leak. diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java index 8f5b5a487bf..63d4cc77587 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java @@ -15,4 +15,9 @@ public DefaultBaseQqsAdapter(String paramString) { public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java index 4adc142ed28..eb85baa8b41 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java @@ -21,4 +21,8 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java index c6fb089063a..0ebd21149a7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(data.getRemoteAddr()); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(data.getRemoteAddr()); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java index 46ed8beee92..29f7b61b6a5 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java @@ -1,9 +1,17 @@ package org.tron.core.services.ratelimiter.adapter; +import org.tron.core.config.args.Args; import org.tron.core.services.ratelimiter.RuntimeData; public interface IRateLimiter { boolean tryAcquire(RuntimeData data); + boolean acquire(RuntimeData data); + + default boolean acquirePermit(RuntimeData data) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(data) + : acquire(data); + } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java index 846a5eb1c4e..62074eac885 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java index 0a29183d762..e7b7f560b29 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java @@ -3,11 +3,15 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class GlobalPreemptibleStrategy extends Strategy { public static final String STRATEGY_PARAM_PERMIT = "permit"; public static final int DEFAULT_PERMIT_NUM = 1; + public static final int DEFAULT_ACQUIRE_TIMEOUT = 2; private Semaphore sp; public GlobalPreemptibleStrategy(String paramString) { @@ -23,15 +27,25 @@ protected Map defaultParam() { return map; } - // Non-blocking: immediately rejects if no permit is available. - // Intentional change from the previous tryAcquire(2, TimeUnit.SECONDS) behaviour: - // blocking the caller for up to 2 s ties up Netty IO / gRPC executor threads and - // masks overload rather than shedding it. All rate-limiting in this stack is now - // non-blocking to keep the thread model consistent with GlobalRateLimiter. + // Non-blocking: immediately rejects if no permit is available. Used when the + // apiNonBlocking switch is on, to shed overload instead of tying up Netty IO / + // gRPC executor threads while waiting for a permit. public boolean tryAcquire() { return sp.tryAcquire(); } + public boolean acquire() { + try { + return sp.tryAcquire(DEFAULT_ACQUIRE_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Restore the interrupt flag and reject — caller must not release a permit + // it never acquired. + logger.error("acquire permit with error: {}", e.getMessage()); + Thread.currentThread().interrupt(); + return false; + } + } + public void release() { sp.release(); } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java index 6589c90fe1d..7ffd1f04eb7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java @@ -22,17 +22,29 @@ public IPQpsStrategy(String paramString) { } public boolean tryAcquire(String ip) { - RateLimiter limiter; + RateLimiter limiter = loadLimiter(ip); + return limiter != null && limiter.tryAcquire(); + } + + public boolean acquire(String ip) { + RateLimiter limiter = loadLimiter(ip); + if (limiter == null) { + return false; + } + limiter.acquire(); + return true; + } + + private RateLimiter loadLimiter(String ip) { try { // cache.get is atomic: only one loader executes per key under concurrent requests, // preventing multiple RateLimiter instances from being created for the same IP. - limiter = ipLimiter.get(ip, this::newRateLimiter); + return ipLimiter.get(ip, this::newRateLimiter); } catch (Exception e) { logger.warn("Failed to load IP rate limiter for {}, denying request: {}", ip, e.getMessage()); - return false; + return null; } - return limiter.tryAcquire(); } private RateLimiter newRateLimiter() { diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java index 7e0466448b3..9116af1b7da 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java @@ -29,4 +29,9 @@ protected Map defaultParam() { public boolean tryAcquire() { return rateLimiter.tryAcquire(); } + + public boolean acquire() { + rateLimiter.acquire(); + return true; + } } \ No newline at end of file diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index d00f334f4ce..d6d3ab236a6 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -415,7 +415,7 @@ node { ## rate limiter config rate.limiter = { - # Every api could only set a specific rate limit strategy. Three non-blocking strategy are supported: + # Every api could only set a specific rate limit strategy. Three strategy are supported: # GlobalPreemptibleAdapter: The number of preemptible resource or maximum concurrent requests globally. # QpsRateLimiterAdapter: qps is the average request count in one second supported by the server, it could be a Double or a Integer. # IPQPSRateLimiterAdapter: similar to the QpsRateLimiterAdapter, qps could be a Double or a Integer. @@ -473,6 +473,9 @@ rate.limiter = { global.qps = 50000 # IP-based global qps, default 10000 global.ip.qps = 10000 + # If true, API rate limiters reject immediately on overload (non-blocking). + # If false (default), callers wait for a permit (blocking, the legacy behaviour). + apiNonBlocking = false } 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 index 1ae341696eb..8cca558d151 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -167,14 +167,14 @@ public void testBuildsEachWhitelistedAdapter() { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); - // tryAcquire returned false — no permit was taken, nothing to release + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); + // acquirePermit returned false — no permit was taken, nothing to release verify(perEndpoint, never()).release(); } } @@ -186,13 +186,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws Exception { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -203,11 +203,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws E @Test public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); servlet.service(request, response); @@ -223,11 +223,11 @@ public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { @Test public void testBothPassPermitReleasedAfterRequest() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); @@ -243,11 +243,11 @@ public void testBothPassPermitReleasedAfterRequest() throws Exception { public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { // No entry added to container — container.get() returns null try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } } diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java index c34d49d9009..8ea0f908899 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java @@ -1,5 +1,10 @@ package org.tron.core.services.ratelimiter; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.RateLimiter; @@ -9,6 +14,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.tron.common.TestConstants; import org.tron.core.config.args.Args; @@ -135,6 +143,104 @@ public void testPerIpLimitsAreIndependent() throws Exception { Assert.assertFalse(GlobalRateLimiter.tryAcquire(runtimeDataFor("2.2.2.2"))); } + /** + * acquire() must drain the IP limiter before the global limiter, mirroring + * tryAcquire(). A reversed order would let one chatty IP consume global + * quota even when its own per-IP budget is exhausted. + */ + @Test + public void testAcquireOrdersIpBeforeGlobal() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + RateLimiter ipMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + Cache seeded = CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build(); + seeded.put("10.0.0.1", ipMock); + injectCache(seeded); + + Assert.assertTrue(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + InOrder inOrder = Mockito.inOrder(ipMock, globalMock); + inOrder.verify(ipMock).acquire(); + inOrder.verify(globalMock).acquire(); + } + + /** + * If the IP limiter cannot be created (cache loader throws), acquire() + * returns false without consuming a global token — same fail-closed + * behaviour as tryAcquire(). + */ + @Test + public void testAcquireDoesNotConsumeGlobalWhenIpLoaderFails() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + // RateLimiter.create(-1.0) throws IllegalArgumentException, so the + // cache loader fails and loadIpLimiter() returns null. + injectIpQps(-1.0); + injectCache(CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build()); + + Assert.assertFalse(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + Mockito.verify(globalMock, never()).acquire(); + } + + /** + * acquirePermit dispatches based on rate.limiter.apiNonBlocking: + * switch on → only tryAcquire runs; switch off → only acquire runs. + * These tests pin that contract on the static dispatcher; the matching + * default-method contract for IRateLimiter is covered in AdaptorTest. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(true); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.acquire(any()), never()); + } + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(false); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.acquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.acquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + } + } + + private static void injectRateLimiter(RateLimiter rl) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("rateLimiter"); + f.setAccessible(true); + f.set(null, rl); + } + + private static void injectCache(Cache cache) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("cache"); + f.setAccessible(true); + f.set(null, cache); + } + + private static void injectIpQps(double qps) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("IP_QPS"); + f.setAccessible(true); + f.set(null, qps); + } + @AfterClass public static void destroy() { Args.clearParam(); diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java index 6cf02a25050..bbc365f3e0b 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java @@ -95,13 +95,13 @@ public void setUp() throws Exception { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); verify(perEndpoint, never()).release(); } } @@ -112,13 +112,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -129,11 +129,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { @Test public void testGlobalRejectedReleasesPreemptiblePermit() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); interceptor.interceptCall(call, headers, next); @@ -153,12 +153,12 @@ public void testGlobalRejectedReleasesPreemptiblePermit() { @Test public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); when(next.startCall(any(), any())).thenThrow(new RuntimeException("handler crash")); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); @@ -176,14 +176,14 @@ public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception @Test public void testListenerReleasesPermitOnComplete() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onComplete(); @@ -199,14 +199,14 @@ public void testListenerReleasesPermitOnComplete() throws Exception { @Test public void testListenerReleasesPermitOnCancel() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onCancel(); @@ -225,11 +225,11 @@ public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } } 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 69a6c688200..5ab85a42bbf 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 @@ -4,12 +4,18 @@ import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ReflectUtils; +import org.tron.core.config.args.Args; +import org.tron.core.services.ratelimiter.RuntimeData; 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; import org.tron.core.services.ratelimiter.strategy.GlobalPreemptibleStrategy; import org.tron.core.services.ratelimiter.strategy.IPQpsStrategy; @@ -17,6 +23,61 @@ public class AdaptorTest { + @Before + public void setUp() { + Args.setParam(new String[0], TestConstants.TEST_CONF); + } + + @AfterClass + public static void tearDown() { + Args.clearParam(); + } + + /** + * IRateLimiter.acquirePermit is a default method that dispatches based on + * rate.limiter.apiNonBlocking. The two cases below pin that contract: with + * the switch on, only tryAcquire is invoked; with the switch off, only + * acquire is invoked. Breaking either direction is a behavioural regression. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(true); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(1, limiter.tryAcquireCount); + Assert.assertEquals(0, limiter.acquireCount); + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(false); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(0, limiter.tryAcquireCount); + Assert.assertEquals(1, limiter.acquireCount); + } + + private static final class CountingRateLimiter implements IRateLimiter { + int tryAcquireCount; + int acquireCount; + + @Override + public boolean tryAcquire(RuntimeData data) { + tryAcquireCount++; + return true; + } + + @Override + public boolean acquire(RuntimeData data) { + acquireCount++; + return true; + } + } + @Test public void testStrategy() { String paramString1 = "qps=5 notExist=6"; From 0dd5139a5f6fe50abf84e6a0392b9dc4581550cc Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 14 May 2026 16:53:42 +0800 Subject: [PATCH 5/7] refactor(config,protocol,ci): optimize config binding (#6762) --- codecov.yml | 38 --- .../common/parameter/CommonParameter.java | 3 - .../core/config/args/CommitteeConfig.java | 116 ++++----- .../tron/core/config/args/EventConfig.java | 72 ++---- .../org/tron/core/config/args/NodeConfig.java | 228 +++++++----------- .../org/tron/core/config/args/Overlay.java | 22 -- .../org/tron/core/config/args/Storage.java | 4 - .../tron/core/config/args/StorageConfig.java | 15 +- .../org/tron/core/config/args/VmConfig.java | 33 +-- common/src/main/resources/reference.conf | 21 +- .../core/config/args/CommitteeConfigTest.java | 8 +- .../core/config/args/EventConfigTest.java | 7 + .../tron/core/config/args/NodeConfigTest.java | 2 - .../tron/core/config/args/VmConfigTest.java | 43 ++-- .../java/org/tron/core/config/args/Args.java | 9 +- .../main/java/org/tron/program/FullNode.java | 4 +- framework/src/main/resources/config.conf | 2 +- .../java/org/tron/common/ParameterTest.java | 1 - .../tron/core/config/args/OverlayTest.java | 40 --- protocol/src/main/protos/api/api.proto | 1 - 20 files changed, 224 insertions(+), 445 deletions(-) delete mode 100644 codecov.yml delete mode 100644 common/src/main/java/org/tron/core/config/args/Overlay.java delete mode 100644 framework/src/test/java/org/tron/core/config/args/OverlayTest.java diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 1b46f3fa8db..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,38 +0,0 @@ -# DEPRECATED: Codecov integration is no longer active. -# Coverage is now handled by JaCoCo + madrapps/jacoco-report in pr-build.yml. -# This file is retained for reference only and can be safely deleted. - -# Post a Codecov comment on pull requests. If don't need comment, use comment: false, else use following -comment: false -#comment: -# # Show coverage diff, flags table, and changed files in the PR comment -# layout: "diff, flags, files" -# # Update existing comment if present, otherwise create a new one -# behavior: default -# # Post a comment even when coverage numbers do not change -# require_changes: false -# # Do not require a base report before posting the comment -# require_base: false -# # Require the PR head commit to have a coverage report -# require_head: true -# # Show both project coverage and patch coverage in the PR comment -# hide_project_coverage: false - -codecov: - # Do not wait for all CI checks to pass before sending notifications - require_ci_to_pass: false - notify: - wait_for_ci: false - -coverage: - status: - project: # PR coverage/project UI - default: - # Compare against the base branch automatically - target: auto - # Allow a small coverage drop tolerance - threshold: 0.02% -# patch: off - patch: # PR coverage/patch UI - default: - target: 60% 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 8bbc52e6d87..7c8c16ed422 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -14,7 +14,6 @@ import org.tron.common.logsfilter.FilterQuery; import org.tron.common.setting.RocksDbSettings; import org.tron.core.Constant; -import org.tron.core.config.args.Overlay; import org.tron.core.config.args.SeedNode; import org.tron.core.config.args.Storage; import org.tron.p2p.P2pConfig; @@ -435,8 +434,6 @@ public class CommonParameter { @Getter public Storage storage; @Getter - public Overlay overlay; - @Getter public SeedNode seedNode; @Getter public EventPluginConfig eventPluginConfig; 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 index 5cd9de842a0..660fa289e3b 100644 --- a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigValue; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -35,29 +36,10 @@ public class CommitteeConfig { 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; } + // "allowPBFT" / "pBFTExpireNum" in config.conf use non-standard casing; they are + // remapped to standard camelCase by normalizeNonStandardKeys() before binding. + private long allowPbft = 0; + private long pbftExpireNum = 20; private long allowTvmFreeze = 0; private long allowTvmVote = 0; private long allowTvmLondon = 0; @@ -85,32 +67,30 @@ public class CommitteeConfig { 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"); - + Config section = normalizeNonStandardKeys(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; } + // "allowPBFT" and "pBFTExpireNum" use non-standard casing that JavaBean Introspector + // cannot derive correctly (setPBFTExpireNum -> property "PBFTExpireNum", not "pBFTExpireNum"). + // Remap them to standard camelCase keys so ConfigBeanFactory binds them normally. + // Config is immutable; withValue() returns a new object. + private static Config normalizeNonStandardKeys(Config section) { + if (section.hasPath("allowPBFT")) { + ConfigValue v = section.getValue("allowPBFT"); + section = section.withValue("allowPbft", v); // rename allowPBFT -> allowPbft + } + if (section.hasPath("pBFTExpireNum")) { + ConfigValue v = section.getValue("pBFTExpireNum"); + section = section.withValue("pbftExpireNum", v); // rename pBFTExpireNum -> pbftExpireNum + } + return section; + } + private void postProcess() { // clamp unfreezeDelayDays to 0-365 if (unfreezeDelayDays < 0) { @@ -121,35 +101,61 @@ private void postProcess() { } // clamp allowDelegateOptimization to 0-1 - if (allowDelegateOptimization < 0) { allowDelegateOptimization = 0; } - if (allowDelegateOptimization > 1) { allowDelegateOptimization = 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; } + 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 < 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; } + 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; } + 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; } + 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; } + 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 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 index 43707956845..f4378682cc3 100644 --- a/common/src/main/java/org/tron/core/config/args/EventConfig.java +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -25,19 +25,15 @@ public class EventConfig { private String server = ""; private String dbconfig = ""; private boolean contractParse = true; - @Getter(lombok.AccessLevel.NONE) + // "native" is a Java reserved word; config key cannot match field name directly. + // @Setter(NONE) prevents ConfigBeanFactory from requiring a "nativeQueue" key. @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) + // Topics list items have optional fields; excluded from auto-binding. + // @Setter(NONE) prevents ConfigBeanFactory from requiring a "topics" key. @Setter(lombok.AccessLevel.NONE) private List topics = new ArrayList<>(); - - public List getTopics() { return topics; } private FilterConfig filter = new FilterConfig(); @Getter @@ -70,62 +66,40 @@ public static class FilterConfig { // Defaults come from reference.conf (loaded globally via Configuration.java) + // TopicConfig fields are optional per item; this fallback ensures all keys exist + // for ConfigBeanFactory binding. Values must match TopicConfig field defaults. + private static final Config TOPIC_DEFAULTS = ConfigFactory.parseString( + "triggerName=\"\", enable=false, topic=\"\", " + + "solidified=false, ethCompatible=false, redundancy=false"); + /** * 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. + *

"native" is a Java reserved word, so the field is named "nativeQueue" and the + * sub-section is read directly after binding. "topics" items may omit optional fields; + * TOPIC_DEFAULTS provides fallback values so ConfigBeanFactory can bind each item. */ 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"); + // remove two keys to construct EventConfig because they cannot be bind automatically, + // we can bind them manually later + Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey); 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"); - } + // "native" sub-section: bind via ConfigBeanFactory when present, use defaults otherwise + ec.nativeQueue = section.hasPath(nativeKey) + ? ConfigBeanFactory.create(section.getConfig(nativeKey), NativeConfig.class) + : new NativeConfig(); - // manually bind topics — each item may have optional fields + // topics: withFallback fills optional fields so ConfigBeanFactory can bind each item 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); + ec.topics.add(ConfigBeanFactory.create( + obj.toConfig().withFallback(TOPIC_DEFAULTS), TopicConfig.class)); } } 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 ea9f26a06a0..82619726b7e 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 @@ -29,47 +29,43 @@ public class NodeConfig { private int syncFetchBatchNum = 2000; private int maxPendingBlockSize = 500; private int validateSignThreadNum = 0; // 0 = auto (availableProcessors) - private int maxConnections = 30; + private int maxConnections = 30; // legacy key maxActiveNodes private int minConnections = 8; private int minActiveConnections = 3; - private int maxConnectionsWithSameIp = 2; + private int maxConnectionsWithSameIp = 2; // legacy key maxActiveNodesWithSameIp private int maxHttpConnectNumber = 50; private int minParticipationRate = 0; 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) - private boolean isOpenFullTcpDisconnect = false; - - public boolean isOpenFullTcpDisconnect() { return isOpenFullTcpDisconnect; } + private boolean openFullTcpDisconnect = false; //rename key // node.discovery.* — HOCON merges into node { discovery { ... } }, auto-bound private DiscoveryConfig discovery = new DiscoveryConfig(); - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private String externalIP = ""; - // node.shutdown.* uses PascalCase keys (BlockTime, BlockHeight, BlockCount) - // that don't match JavaBean naming. Excluded, read manually. - @Getter(lombok.AccessLevel.NONE) + // node.shutdown.* uses PascalCase nested keys (shutdown.BlockTime, etc.). + // These are optional (not in reference.conf), so @Setter(NONE) prevents ConfigBeanFactory + // from requiring the keys; values are read manually in fromConfig(). @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 externalIP; } - public String getShutdownBlockTime() { return shutdownBlockTime; } - public long getShutdownBlockHeight() { return shutdownBlockHeight; } - public long getShutdownBlockCount() { return shutdownBlockCount; } + public boolean isDiscoveryEnable() { + return discovery.isEnable(); + } + + public boolean isDiscoveryPersist() { + return discovery.isPersist(); + } + + public String getDiscoveryExternalIp() { + return discovery.getExternal().getIp(); + } + private int inactiveThreshold = 600; private boolean metricsEnable = false; private int blockProducedTimeOut = 50; @@ -90,14 +86,14 @@ public class NodeConfig { private boolean unsolidifiedBlockCheck = false; private int maxUnsolidifiedBlocks = 54; private String zenTokenId = "000000"; - @Getter(lombok.AccessLevel.NONE) + // allowShieldedTransactionApi is optional (commented out in reference.conf) and has a + // legacy key fallback; @Setter(NONE) prevents ConfigBeanFactory from requiring the key. @Setter(lombok.AccessLevel.NONE) private boolean allowShieldedTransactionApi = false; + + //deprecate key 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(); @@ -105,11 +101,21 @@ public class NodeConfig { private SolidityConfig solidity = new SolidityConfig(); // Convenience getters for backward compatibility with applyNodeConfig - public int getListenPort() { return listen.getPort(); } - public int getFetchBlockTimeout() { return fetchBlock.getTimeout(); } - public int getSolidityThreads() { return solidity.getThreads(); } - public int getValidContractProtoThreads() { return validContractProto.getThreads(); } - public boolean isAllowShieldedTransactionApi() { return allowShieldedTransactionApi; } + public int getListenPort() { + return listen.getPort(); + } + + public int getFetchBlockTimeout() { + return fetchBlock.getTimeout(); + } + + public int getSolidityThreads() { + return solidity.getThreads(); + } + + public int getValidContractProtoThreads() { + return validContractProto.getThreads(); + } // ---- List fields (manually read) ---- private List active = new ArrayList<>(); @@ -136,43 +142,58 @@ public class NodeConfig { @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 FetchBlockConfig { + private int timeout = 500; } @Getter @Setter public static class SolidityConfig { + private int threads = 0; // 0 = auto (availableProcessors) } @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; @@ -180,63 +201,21 @@ public static class HttpConfig { private long maxMessageSize = 4194304; private int maxNestingDepth = 100; private int maxTokenCount = 100_000; - // 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; @@ -254,34 +233,14 @@ public void setPBFTPort(int v) { @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; @@ -295,6 +254,7 @@ public void setHttpPBFTPort(int v) { @Getter @Setter public static class NodeBackupConfig { + private int priority = 0; private int port = 10001; private int keepAliveInterval = 3000; @@ -304,6 +264,7 @@ public static class NodeBackupConfig { @Getter @Setter public static class DynamicConfigSection { + private boolean enable = false; private long checkInterval = 600; } @@ -311,6 +272,7 @@ public static class DynamicConfigSection { @Getter @Setter public static class DnsConfig { + private List treeUrls = new ArrayList<>(); private boolean publish = false; private String dnsDomain = ""; @@ -327,40 +289,20 @@ public static class DnsConfig { 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, 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) { - // Normalize human-readable size values (e.g. "4m") to numeric bytes so - // ConfigBeanFactory's primitive int/long binding succeeds; same step - // enforces non-negative and <= Integer.MAX_VALUE before bean creation - // so failures point at the user-facing config path. - Config section = normalizeMaxMessageSizes(config).getConfig("node"); + Config section = normalizeNonStandardKeys( + normalizeMaxMessageSizes(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. + // descriptive path on any `= null` value 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")) { @@ -377,11 +319,6 @@ public static NodeConfig fromConfig(Config config) { nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); } - nc.externalIP = getString(section, "discovery.external.ip", ""); - if ("null".equalsIgnoreCase(nc.externalIP)) { - nc.externalIP = ""; - } - // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. if (section.hasPath("allowShieldedTransactionApi")) { nc.allowShieldedTransactionApi = @@ -394,14 +331,13 @@ public static NodeConfig fromConfig(Config config) { + "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; - + // node.shutdown.* — optional PascalCase nested keys, not in reference.conf by default + nc.shutdownBlockTime = section.hasPath("shutdown.BlockTime") + ? section.getString("shutdown.BlockTime") : ""; + nc.shutdownBlockHeight = section.hasPath("shutdown.BlockHeight") + ? section.getLong("shutdown.BlockHeight") : -1; + nc.shutdownBlockCount = section.hasPath("shutdown.BlockCount") + ? section.getLong("shutdown.BlockCount") : -1; nc.postProcess(); return nc; @@ -513,11 +449,31 @@ private static String getString(Config config, String path, String defaultValue) return config.hasPath(path) ? config.getString(path) : defaultValue; } - // Pre-normalize size paths so ConfigBeanFactory's primitive int/long binding succeeds - // for human-readable values like "4m" / "128MB". For each maxMessageSize key, parse - // via getMemorySize, validate non-negative and <= Integer.MAX_VALUE, and write the - // numeric byte value back into the Config tree. Validation errors propagate before - // bean creation so the failure points at the user-facing config path. + /** + * "isOpenFullTcpDisconnect" config key has an "is" prefix that the JavaBean Introspector + * strips from boolean getter names, so the derived property is "openFullTcpDisconnect". + * "discovery.external.ip" may be HOCON null or the string "null"; both normalize to "". + */ + private static Config normalizeNonStandardKeys(Config section) { + if (section.hasPath("isOpenFullTcpDisconnect")) { + section = section.withValue("openFullTcpDisconnect", + section.getValue("isOpenFullTcpDisconnect")); + } + String externalIpPath = "discovery.external.ip"; + if (section.getIsNull(externalIpPath) + || "null".equalsIgnoreCase(section.getString(externalIpPath))) { + section = section.withValue(externalIpPath, ConfigValueFactory.fromAnyRef("")); + } + return section; + } + + /** + * Pre-normalize size paths so ConfigBeanFactory's primitive int/long binding succeeds + * for human-readable values like "4m" / "128MB". For each maxMessageSize key, parse + * via getMemorySize, validate non-negative and <= Integer.MAX_VALUE, and write the + * numeric byte value back into the Config tree. Validation errors propagate before + * bean creation so the failure points at the user-facing config path. + */ private static Config normalizeMaxMessageSizes(Config config) { String[] paths = { "node.rpc.maxMessageSize", diff --git a/common/src/main/java/org/tron/core/config/args/Overlay.java b/common/src/main/java/org/tron/core/config/args/Overlay.java deleted file mode 100644 index bdaa40724c7..00000000000 --- a/common/src/main/java/org/tron/core/config/args/Overlay.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.tron.core.config.args; - -import lombok.Getter; -import org.apache.commons.lang3.Range; - -public class Overlay { - - @Getter - private int port; - - /** - * Monitor port number. - */ - public void setPort(final int port) { - Range range = Range.between(0, 65535); - if (!range.contains(port)) { - throw new IllegalArgumentException("Port(" + port + ") must in [0, 65535]"); - } - - this.port = port; - } -} 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 782a0ef07c8..64a9efab7f1 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 @@ -201,10 +201,6 @@ private static void applyPropertyOptions(StorageConfig.PropertyConfig pc, Option dbOptions.maxOpenFiles(pc.getMaxOpenFiles()); } - - /** - * Set propertyMap of Storage object from Config via StorageConfig bean. - */ /** * Set propertyMap from StorageConfig bean list. No Config parameter needed. */ 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 index 3d7046ebae2..5f8efffb9f3 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -39,30 +39,19 @@ public class StorageConfig { // 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(NONE): optional keys commented out in reference.conf; ConfigBeanFactory + // would throw if it required them. Values are assigned in fromConfig(). @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 { 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 index 00ba85aa6cc..3ff1136f33e 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -2,24 +2,18 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; -import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** * VM configuration bean. Field names match config.conf keys under the "vm" section. - * Most fields are bound automatically via ConfigBeanFactory; opt-in fields that - * must stay absent from reference.conf are bound manually after hasPath checks. */ @Slf4j @Getter @Setter public class VmConfig { - private static final String CONSTANT_CALL_TIMEOUT_MS_KEY = "constantCallTimeoutMs"; - static final long MAX_CONSTANT_CALL_TIMEOUT_MS = Long.MAX_VALUE / 1_000L; - private boolean supportConstant = false; private long maxEnergyLimitForConstant = 100_000_000L; private int lruCacheSize = 500; @@ -32,10 +26,6 @@ public class VmConfig { private boolean saveInternalTx = false; private boolean saveFeaturedInternalTx = false; private boolean saveCancelAllUnfreezeV2Details = false; - // Excluded from ConfigBeanFactory binding (no setter): the property is - // intentionally absent from reference.conf so {@code Config#hasPath} alone - // signals operator opt-in. Bound manually in {@link #fromConfig}. - @Setter(AccessLevel.NONE) private long constantCallTimeoutMs = 0L; /** @@ -46,11 +36,11 @@ public class VmConfig { public static VmConfig fromConfig(Config config) { Config vmSection = config.getConfig("vm"); VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); - vmConfig.postProcess(vmSection); + vmConfig.postProcess(); return vmConfig; } - private void postProcess(Config vmSection) { + private void postProcess() { // clamp maxEnergyLimitForConstant if (maxEnergyLimitForConstant < 3_000_000L) { maxEnergyLimitForConstant = 3_000_000L; @@ -71,22 +61,9 @@ private void postProcess(Config vmSection) { + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); } - // constantCallTimeoutMs is excluded from ConfigBeanFactory binding (no - // setter) and intentionally absent from reference.conf, so hasPath alone - // tells us whether the operator opted in. Only positive values that can be - // safely converted to microseconds are valid. - if (vmSection.hasPath(CONSTANT_CALL_TIMEOUT_MS_KEY)) { - long value = vmSection.getLong(CONSTANT_CALL_TIMEOUT_MS_KEY); - if (value <= 0L) { - throw new IllegalArgumentException( - "vm.constantCallTimeoutMs must be > 0 when configured, got " + value); - } - if (value > MAX_CONSTANT_CALL_TIMEOUT_MS) { - throw new IllegalArgumentException( - "vm.constantCallTimeoutMs must be <= " + MAX_CONSTANT_CALL_TIMEOUT_MS - + " to fit VM deadline conversion, got " + value); - } - constantCallTimeoutMs = value; + if (constantCallTimeoutMs < 0 || constantCallTimeoutMs > Long.MAX_VALUE / 1000) { + throw new IllegalArgumentException("vm.constantCallTimeoutMs must be >= 0 and <= " + + Long.MAX_VALUE / 1000 + " to fit VM deadline conversion, got " + constantCallTimeoutMs); } } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 3f41c556f96..0864f4d5126 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -25,18 +25,16 @@ # Key naming rules (required for ConfigBeanFactory auto-binding): # - Use standard camelCase: maxConnections, syncFetchBatchNum, etc. # -# Keys that cannot auto-bind (handled manually in bean fromConfig): +# Keys that cannot auto-bind (handled via normalizeNonStandardKeys() or manual reads): # -# 1. committee.pBFTExpireNum — lowercase "p" then uppercase "BFT": -# setPBFTExpireNum -> property "PBFTExpireNum" (capital P), -# mismatches config key "pBFTExpireNum" (lowercase p). +# 1. committee.pBFTExpireNum / committee.allowPBFT — normalized to camelCase in +# CommitteeConfig.normalizeNonStandardKeys() before ConfigBeanFactory binding. # -# 2. node.isOpenFullTcpDisconnect — boolean "is" prefix: -# getter isOpenFullTcpDisconnect() -> property "openFullTcpDisconnect", -# mismatches config key "isOpenFullTcpDisconnect". +# 2. node.isOpenFullTcpDisconnect — normalized to "openFullTcpDisconnect" in +# NodeConfig.normalizeNonStandardKeys() before ConfigBeanFactory binding. # -# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — PascalCase keys: -# setBlockTime -> property "blockTime", mismatches "BlockTime". +# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — optional PascalCase nested keys; +# read manually in NodeConfig.fromConfig() after ConfigBeanFactory binding. # # ============================================================================= @@ -166,8 +164,6 @@ node.metrics = { 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 @@ -702,6 +698,9 @@ vm = { # Max retry time for executing transaction in estimating energy estimateEnergyMaxRetry = 3 + + # Max TVM execution time (ms) for constant calls. 0 means no effect + constantCallTimeoutMs = 0 } # Governance proposal toggle parameters. All default to 0 (disabled). 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 index 962b6a349ab..559198100fb 100644 --- a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java @@ -20,8 +20,8 @@ private static Config withRef() { public void testDefaults() { CommitteeConfig cc = CommitteeConfig.fromConfig(withRef()); assertEquals(0, cc.getAllowCreationOfContracts()); - assertEquals(0, cc.getAllowPBFT()); - assertEquals(20, cc.getPBFTExpireNum()); + assertEquals(0, cc.getAllowPbft()); + assertEquals(20, cc.getPbftExpireNum()); assertEquals(0, cc.getUnfreezeDelayDays()); assertEquals(0, cc.getAllowDynamicEnergy()); } @@ -32,8 +32,8 @@ public void testFromConfig() { "committee { allowCreationOfContracts = 1, allowPBFT = 1, pBFTExpireNum = 30 }"); CommitteeConfig cc = CommitteeConfig.fromConfig(config); assertEquals(1, cc.getAllowCreationOfContracts()); - assertEquals(1, cc.getAllowPBFT()); - assertEquals(30, cc.getPBFTExpireNum()); + assertEquals(1, cc.getAllowPbft()); + assertEquals(30, cc.getPbftExpireNum()); } @Test 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 index 361d9f48581..ca0cbefaddd 100644 --- a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -79,4 +79,11 @@ public void testFilter() { assertEquals(2, ec.getFilter().getContractAddress().size()); assertEquals(1, ec.getFilter().getContractTopic().size()); } + + @Test + public void testTopicsEmptyList() { + EventConfig ec = EventConfig.fromConfig(withRef( + "event.subscribe.topics = []")); + assertTrue(ec.getTopics().isEmpty()); + } } 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 d4fbc05e730..bbc2d2475ee 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 @@ -245,8 +245,6 @@ public void testValidContractProtoThreadsExplicitPreserved() { @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()); } 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 index e406ef24e7b..99015a8c012 100644 --- a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -2,10 +2,12 @@ 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.Assert; import org.junit.Test; public class VmConfigTest { @@ -90,8 +92,8 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { } // =========================================================================== - // Constant-call timeout (issue #6681). The validation rule: any positive - // value that fits VM deadline conversion is accepted, but zero/negative is + // Constant-call timeout (issue #6681). The validation rule: any zero or positive + // value that fits VM deadline conversion is accepted, but negative is // rejected ONLY when the operator explicitly set the property in their // config. Absence keeps the in-Java default (0L = "share the // block-processing deadline"). @@ -99,7 +101,7 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { @Test public void testConstantCallTimeoutDefaultWhenAbsent() { - // No path in the config, no entry in reference.conf -> default 0L kept, + // reference.conf default is 0; absence of a user override keeps that default; // no validation triggered. VmConfig vm = VmConfig.fromConfig(withRef()); assertEquals(0L, vm.getConstantCallTimeoutMs()); @@ -107,6 +109,8 @@ public void testConstantCallTimeoutDefaultWhenAbsent() { @Test public void testConstantCallTimeoutAcceptsAnyPositiveValue() { + assertEquals(0L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 0 }")).getConstantCallTimeoutMs()); assertEquals(1L, VmConfig.fromConfig( withRef("vm { constantCallTimeoutMs = 1 }")).getConstantCallTimeoutMs()); assertEquals(50L, VmConfig.fromConfig( @@ -117,39 +121,20 @@ public void testConstantCallTimeoutAcceptsAnyPositiveValue() { withRef("vm { constantCallTimeoutMs = 5000 }")).getConstantCallTimeoutMs()); } - @Test - public void testConstantCallTimeoutZeroRejectedWhenExplicitlyConfigured() { - // Operator wrote `= 0` in config -> treated as a misconfiguration even - // though it equals the in-Java default. Forces an explicit positive value. - try { - VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = 0 }")); - org.junit.Assert.fail("expected IllegalArgumentException for explicit 0"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("constantCallTimeoutMs")); - } - } - @Test public void testConstantCallTimeoutNegativeRejected() { - try { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = -1 }")); - org.junit.Assert.fail("expected IllegalArgumentException for negative ms"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("constantCallTimeoutMs")); - } + }); + Assert.assertTrue(thrown.getMessage().contains("constantCallTimeoutMs")); } @Test public void testConstantCallTimeoutOverflowRejected() { - long value = VmConfig.MAX_CONSTANT_CALL_TIMEOUT_MS + 1L; - try { + long value = Long.MAX_VALUE / 1000 + 1L; + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = " + value + " }")); - org.junit.Assert.fail("expected IllegalArgumentException for overflowing ms"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("deadline conversion")); - } + }); + Assert.assertTrue(thrown.getMessage().contains("deadline conversion")); } } 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 ec808267c75..8d8e2500c9f 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 @@ -467,8 +467,8 @@ private static void applyCommitteeConfig(CommitteeConfig cc) { PARAMETER.allowProtoFilterNum = cc.getAllowProtoFilterNum(); PARAMETER.allowAccountStateRoot = cc.getAllowAccountStateRoot(); PARAMETER.changedDelegation = cc.getChangedDelegation(); - PARAMETER.allowPBFT = cc.getAllowPBFT(); - PARAMETER.pBFTExpireNum = cc.getPBFTExpireNum(); + PARAMETER.allowPBFT = cc.getAllowPbft(); + PARAMETER.pBFTExpireNum = cc.getPbftExpireNum(); PARAMETER.allowTvmFreeze = cc.getAllowTvmFreeze(); PARAMETER.allowTvmVote = cc.getAllowTvmVote(); PARAMETER.allowTvmLondon = cc.getAllowTvmLondon(); @@ -610,10 +610,7 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.maxHttpConnectNumber = nc.getMaxHttpConnectNumber(); PARAMETER.netMaxTrxPerSecond = nc.getNetMaxTrxPerSecond(); - if (StringUtils.isEmpty(PARAMETER.trustNodeAddr)) { - String trustNode = nc.getTrustNode(); - PARAMETER.trustNodeAddr = StringUtils.isEmpty(trustNode) ? null : trustNode; - } + PARAMETER.trustNodeAddr = nc.getTrustNode(); PARAMETER.validateSignThreadNum = nc.getValidateSignThreadNum(); PARAMETER.walletExtensionApi = nc.isWalletExtensionApi(); diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 308cb9a1c69..96b9f73d577 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -1,8 +1,8 @@ package org.tron.program; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.util.ObjectUtils; import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; @@ -35,7 +35,7 @@ public static void main(String[] args) { } if (parameter.isSolidityNode()) { logger.info("Solidity node is running."); - if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) { + if (StringUtils.isEmpty(parameter.getTrustNodeAddr())) { throw new TronError(new IllegalArgumentException("Trust node is not set."), TronError.ErrCode.SOLID_NODE_INIT); } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index d6d3ab236a6..0686890f030 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -750,7 +750,7 @@ vm = { # Omit the property entirely to keep the default behaviour of sharing the # block-processing deadline. Migration note: if previously running --debug # to extend constant calls, switch to this option (--debug also extends - # block-processing, which is unsafe; see issue #6266). + # block-processing, which is unsafe; see issue #6266). Default: 0. # constantCallTimeoutMs = 100 } diff --git a/framework/src/test/java/org/tron/common/ParameterTest.java b/framework/src/test/java/org/tron/common/ParameterTest.java index 563f487f635..91bb580a3b4 100644 --- a/framework/src/test/java/org/tron/common/ParameterTest.java +++ b/framework/src/test/java/org/tron/common/ParameterTest.java @@ -216,7 +216,6 @@ public void testCommonParameter() { assertEquals(1000, parameter.getRateLimiterGlobalQps()); parameter.setRateLimiterGlobalIpQps(100); assertEquals(100, parameter.getRateLimiterGlobalIpQps()); - assertNull(parameter.getOverlay()); assertNull(parameter.getEventPluginConfig()); assertNull(parameter.getEventFilter()); parameter.setCryptoEngine(ECKey_ENGINE); diff --git a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java b/framework/src/test/java/org/tron/core/config/args/OverlayTest.java deleted file mode 100644 index 1b7045c5b21..00000000000 --- a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 this program. If not, see . - */ - -package org.tron.core.config.args; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class OverlayTest { - - private Overlay overlay = new Overlay(); - - @Before - public void setOverlay() { - overlay.setPort(8080); - } - - @Test(expected = IllegalArgumentException.class) - public void whenSetOutOfBoundsPort() { - overlay.setPort(-1); - } - - @Test - public void getOverlay() { - Assert.assertEquals(8080, overlay.getPort()); - } -} diff --git a/protocol/src/main/protos/api/api.proto b/protocol/src/main/protos/api/api.proto index 6082d989182..8b79c8cb0d3 100644 --- a/protocol/src/main/protos/api/api.proto +++ b/protocol/src/main/protos/api/api.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package protocol; import "core/Tron.proto"; -import "google/api/annotations.proto"; import "core/contract/asset_issue_contract.proto"; import "core/contract/account_contract.proto"; From 260585c9397b4b6c0921ad097483533e61f813db Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 15 May 2026 15:02:21 +0800 Subject: [PATCH 6/7] fix(jsonrpc): make the JSON-RPC output more compliant with specification (#6763) --- .../core/services/jsonrpc/JsonRpcServlet.java | 49 +++++- .../services/jsonrpc/JsonRpcServletTest.java | 154 ++++++++++++++++++ 2 files changed, 196 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 2093930ca98..29869403988 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,6 +1,8 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -30,7 +32,19 @@ @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { - private static final ObjectMapper MAPPER = new ObjectMapper(); + // Snapshot of node.http.maxNestingDepth / maxTokenCount at class-load time (after Args.setParam). + private static final ObjectMapper MAPPER = buildMapper(); + + private static ObjectMapper buildMapper() { + CommonParameter p = CommonParameter.getInstance(); + JsonFactory factory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(p.getMaxNestingDepth()) + .maxTokenCount(p.getMaxTokenCount()) + .build()) + .build(); + return new ObjectMapper(factory); + } private enum JsonRpcError { PARSE_ERROR(-32700), @@ -97,11 +111,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rootNode = MAPPER.readTree(body); if (rootNode == null || rootNode.isMissingNode()) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); return; } } catch (JsonProcessingException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); + return; + } + + if (!rootNode.isObject() && !rootNode.isArray()) { + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); return; } @@ -159,8 +178,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes JsonNode subRequest = rootNode.get(i); if (overflow) { - // Notifications (no "id") do not get a response even on overflow. - if (subRequest.has("id")) { + if (!subRequest.isObject()) { + batchResult.add(buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null)); + } else if (subRequest.has("id")) { + // Notifications (no "id") do not get a response even on overflow. batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE, "Response exceeds the limit of " + maxResponseSize + " bytes", subRequest.get("id"))); @@ -168,6 +189,19 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes continue; } + if (!subRequest.isObject()) { + ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null); + byte[] errBytes = MAPPER.writeValueAsBytes(errNode); + int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0); + if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { + overflow = true; + } else { + accumulatedSize += addition; + } + batchResult.add(errNode); + continue; + } + byte[] subBody; try { subBody = MAPPER.writeValueAsBytes(subRequest); @@ -213,13 +247,14 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes // JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects. if (batchResult.isEmpty()) { + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(0); return; } byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(finalBytes.length); resp.getOutputStream().write(finalBytes); @@ -261,7 +296,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { bytes = MAPPER.writeValueAsBytes(errorObj); } - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index fa45ca48876..b66298d6779 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -245,6 +245,160 @@ public void normalRequest_commitsRpcServerResponse() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + // --- Content-Type header: must be application/json-rpc (no charset suffix) --- + + @Test + public void errorResponse_contentTypeIsApplicationJsonRpc() throws Exception { + MockHttpServletResponse resp = doPost("not valid json"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void batchResponse_contentTypeIsApplicationJsonRpc() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}]"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void allNotificationBatch_contentTypeIsApplicationJsonRpc() throws Exception { + // notification: rpcServer returns 0 bytes → empty batchResult → early return path + doAnswer(inv -> 0).when(mockRpcServer) + .handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); + assertEquals(200, resp.getStatus()); + assertEquals(0, resp.getContentLength()); + assertEquals("application/json-rpc", resp.getContentType()); + } + + // --- Primitive root node → Invalid Request (-32600), id must be JSON null --- + + @Test + public void primitiveRootNull_returnsInvalidRequestWithJsonNullId() throws Exception { + MockHttpServletResponse resp = doPost("null"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue("id must be JSON null, not the string \"null\"", body.get("id").isNull()); + assertFalse("id must not be a string", body.get("id").isTextual()); + } + + @Test + public void primitiveRootBoolean_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("true"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootNumber_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("123"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootString_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("\"hello\""); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + // --- Non-object element inside a batch → Invalid Request per element --- + + @Test + public void batchWithNestedArray_returnsInvalidRequestArray() throws Exception { + MockHttpServletResponse resp = doPost("[[]]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(1, body.size()); + assertEquals(-32600, body.get(0).get("error").get("code").asInt()); + assertTrue("id in batch error must be JSON null", body.get(0).get("id").isNull()); + } + + @Test + public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}, []]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(2, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); + assertEquals(-32600, body.get(1).get("error").get("code").asInt()); + } + + @Test + public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[42, \"foo\", true]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(3, body.size()); + for (int i = 0; i < 3; i++) { + assertEquals(-32600, body.get(i).get("error").get("code").asInt()); + } + } + + // --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced --- + + @Test + public void excessivelyNestedRequest_returnsParseError() throws Exception { + int limit = CommonParameter.getInstance().getMaxNestingDepth(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i <= limit; i++) { + sb.append('['); + } + sb.append('0'); + for (int i = 0; i <= limit; i++) { + sb.append(']'); + } + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void tooManyTokens_returnsParseError() throws Exception { + int limit = CommonParameter.getInstance().getMaxTokenCount(); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < limit; i++) { + if (i > 0) { + sb.append(','); + } + sb.append('0'); + } + sb.append(']'); + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + // --- helpers --- private MockHttpServletResponse doPost(String body) throws Exception { From 2e2f3e01242b4bb33c4c653ecbf6e40e2284ff19 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 19 May 2026 18:50:15 +0800 Subject: [PATCH 7/7] ci(config): enforce lowerCamelCase and max depth in reference.conf Add a CI gate that scans common/src/main/resources/reference.conf and fails the build when any new key violates lowerCamelCase (^[a-z][a-zA-Z0-9]*$ per dot-separated segment) or exceeds the maximum hierarchy depth of 5. Runs as a new step in the existing checkstyle job of pr-check.yml (no extra runner spin-up). Four legacy PBFT* keys are grandfathered via an in-script allowlist so the gate fails only on new violations. The script emits a consolidated GHA error annotation listing every offending key/line, and uses sys.exit(1) to drive step failure. --- .github/scripts/check_reference_conf.py | 210 ++++++++++++++++++++++++ .github/workflows/pr-check.yml | 6 + 2 files changed, 216 insertions(+) create mode 100644 .github/scripts/check_reference_conf.py diff --git a/.github/scripts/check_reference_conf.py b/.github/scripts/check_reference_conf.py new file mode 100644 index 00000000000..8d61da13f1b --- /dev/null +++ b/.github/scripts/check_reference_conf.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Validate java-tron reference.conf key names and hierarchy depth. + +Rules enforced: + 1. Every dot-separated segment of every key path must match ^[a-z][a-zA-Z0-9]*$ + (lowerCamelCase: starts lowercase, letters/digits only). + 2. Total path depth must be <= MAX_DEPTH. + 3. ALLOWLIST entries are exempt from the format rule (legacy keys that ship in + user configs; renaming would break compatibility). + +Exit code: 0 if clean, 1 if any violation remains after allowlist filtering. +Findings are printed to stdout; the per-line failure summary goes to stderr. + +CI integration: this script is invoked by the `Validate reference.conf key +names and depth` step of the `checkstyle` job in `.github/workflows/pr-check.yml`. +The non-zero exit on violations (`sys.exit(1)` below) is what makes that step +fail — there is intentionally NO extra `exit 1` in the workflow shell wrapper. +A single GHA `::error` workflow command is also emitted unconditionally (not +gated on the GITHUB_ACTIONS env var) so local runs produce the same output as +CI, simplifying dev iteration; the leading `::` is harmless noise locally. +""" +import re +import sys +from pathlib import Path + +MAX_DEPTH = 5 +KEY_REGEX = re.compile(r'^[a-z][a-zA-Z0-9]*$') +ALLOWLIST = { + "node.http.PBFTEnable", + "node.http.PBFTPort", + "node.rpc.PBFTEnable", + "node.rpc.PBFTPort", +} + + +def extract_keys(src: str): + """Yield (line_number, full_dotted_path) for every assignment/object key. + + The scanner: + - tracks brace/bracket frames so array element scopes don't append to the + path prefix; + - skips "..."-quoted strings (with \\ escapes) — critical because values + like `url = "http://x"` contain `//` that must not be read as a comment; + - strips `#` and `//` line comments only outside string literals; + - recognises a key as an identifier (possibly dotted) followed by '=', ':', + or '{'. + """ + ident_re = re.compile(r'[A-Za-z_][A-Za-z0-9_\-]*(?:\.[A-Za-z_][A-Za-z0-9_\-]*)*') + frames = [{"kind": "obj", "prefix": []}] + line = 1 + pos = 0 + n = len(src) + while pos < n: + c = src[pos] + if c == '\n': + line += 1 + pos += 1 + continue + if c in ' \t\r,': + pos += 1 + continue + if c == '#': + while pos < n and src[pos] != '\n': + pos += 1 + continue + if c == '/' and pos + 1 < n and src[pos + 1] == '/': + while pos < n and src[pos] != '\n': + pos += 1 + continue + if c == '{': + # anonymous object (e.g. array element); inherits current prefix + # for arrays, prefix should NOT extend; for nested objects without + # a key, just preserve prefix unchanged. + frames.append({"kind": "obj", "prefix": list(frames[-1]["prefix"])}) + pos += 1 + continue + if c == '[': + frames.append({"kind": "arr", "prefix": list(frames[-1]["prefix"])}) + pos += 1 + continue + if c == '}' or c == ']': + if len(frames) > 1: + frames.pop() + pos += 1 + continue + if c == '"': + pos += 1 + while pos < n: + if src[pos] == '\\': + if pos + 1 < n and src[pos + 1] == '\n': + line += 1 + pos += 2 + continue + if src[pos] == '"': + pos += 1 + break + if src[pos] == '\n': + line += 1 + pos += 1 + continue + m = ident_re.match(src, pos) + if m: + ident = m.group(0) + end = m.end() + p2 = end + while p2 < n and src[p2] in ' \t': + p2 += 1 + if p2 < n and src[p2] in '=:{': + parts = ident.split('.') + full_parts = list(frames[-1]["prefix"]) + parts + yield (line, '.'.join(full_parts), parts) + if src[p2] == '{': + frames.append({"kind": "obj", "prefix": full_parts}) + pos = p2 + 1 + else: + pos = p2 + 1 + continue + pos = end + continue + pos += 1 + + +def main(argv): + if len(argv) != 2: + print(f"usage: {argv[0]} ", file=sys.stderr) + return 2 + path = Path(argv[1]) + if not path.is_file(): + print(f"error: file not found: {path}", file=sys.stderr) + return 2 + + src = path.read_text(encoding="utf-8") + format_violations = [] + depth_violations = [] + seen_paths = set() + + for line, full_path, parts in extract_keys(src): + if full_path in seen_paths: + continue + seen_paths.add(full_path) + + if full_path not in ALLOWLIST: + for seg in parts: + if not KEY_REGEX.match(seg): + format_violations.append((line, full_path, seg)) + break + + depth = len(full_path.split('.')) + if depth > MAX_DEPTH: + depth_violations.append((line, full_path, depth)) + + format_violations.sort(key=lambda v: (v[1], v[0])) + depth_violations.sort(key=lambda v: (v[1], v[0])) + + lines_out = [] + if format_violations: + lines_out.append( + f"Format violations ({len(format_violations)}) — " + f"each segment must match {KEY_REGEX.pattern}:" + ) + for line, full_path, seg in format_violations: + lines_out.append(f" format: {full_path} (segment: '{seg}', line {line})") + if depth_violations: + if lines_out: + lines_out.append("") + lines_out.append( + f"Depth violations ({len(depth_violations)}) — max depth is {MAX_DEPTH}:" + ) + for line, full_path, depth in depth_violations: + lines_out.append( + f" depth: {full_path} (depth={depth}, line {line}, max={MAX_DEPTH})" + ) + + if format_violations or depth_violations: + print("\n".join(lines_out)) + print() + # Emit ONE consolidated GHA workflow annotation with the file path + # attached so the PR Checks panel and Files Changed view link to + # reference.conf. All offending entries are packed into the annotation + # body via %0A (GHA's newline escape) so the entries are visible in the + # annotation summary, not just in the job log. + entries = [] + for line, full_path, seg in format_violations: + entries.append( + f"format: {full_path} (segment '{seg}', line {line})" + ) + for line, full_path, depth in depth_violations: + entries.append( + f"depth: {full_path} (depth={depth}, line {line}, max={MAX_DEPTH})" + ) + body = ( + f"reference.conf has {len(format_violations)} format + " + f"{len(depth_violations)} depth violation(s):%0A" + + "%0A".join(entries) + ) + print(f"::error file={path},title=reference.conf::{body}") + print( + f"FAIL: {len(format_violations)} format + {len(depth_violations)} depth " + f"violation(s) in {path}", + file=sys.stderr, + ) + # The CI step relies on this non-zero exit to fail — see module docstring. + return 1 + + print(f"OK: {path} — {len(seen_paths)} keys, all lowerCamelCase, depth <= {MAX_DEPTH}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 19425209bbc..a466890f74b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -103,6 +103,12 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Validate reference.conf key names and depth + shell: bash + run: | + python3 .github/scripts/check_reference_conf.py \ + common/src/main/resources/reference.conf + - name: Set up JDK 17 uses: actions/setup-java@v5 with: