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: 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 3fe1e878ffb..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; @@ -411,6 +410,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; @@ -432,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 ac1731de2dc..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,25 +25,21 @@ 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 @Setter public static class NativeConfig { - private boolean useNativeQueue = true; + private boolean useNativeQueue = false; private int bindport = 5555; private int sendqueuelength = 1000; } @@ -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/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/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 688e1590788..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 @@ -451,6 +447,7 @@ rate.limiter = { global.qps = 50000 global.ip.qps = 10000 global.api.qps = 1000 + apiNonBlocking = false } seed.node = { @@ -701,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). @@ -758,7 +758,7 @@ event.subscribe = { enable = false native = { - useNativeQueue = true + useNativeQueue = false bindport = 5555 sendqueuelength = 1000 } 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/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/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/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/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 2d6660f9a6a..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 @@ -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()); @@ -325,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(); @@ -343,21 +347,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; } @@ -463,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(); @@ -606,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(); @@ -770,9 +771,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/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/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/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/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 d00f334f4ce..0686890f030 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 } @@ -747,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/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(); 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/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/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); 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/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 { 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"; 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";