From 0b9c4ca193462afb251767c65fd380c6d2db3dde Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 1 Jun 2026 18:36:17 +0800 Subject: [PATCH 01/13] ci: add comment coverage gate for reference.conf Add check_reference_comments.py, a line-oriented Python script that enforces every key in reference.conf carries an inline or immediately- preceding comment. Wire it into pr-check.yml as a new CI step directly after the existing key-format gate. Annotate all previously undocumented keys in reference.conf with inline # / // comments. The committee block now includes the /wallet/getchainparameters API key name and ProposalType ID for each governance parameter. genesis.block array elements (assets, witnesses) carry field-level inline comments derived from the existing block descriptions; there is no genesis.block exemption. --- .github/scripts/check_reference_comments.py | 358 ++++++++++++++++++++ .github/workflows/pr-check.yml | 6 + common/src/main/resources/reference.conf | 289 ++++++++-------- 3 files changed, 513 insertions(+), 140 deletions(-) create mode 100644 .github/scripts/check_reference_comments.py diff --git a/.github/scripts/check_reference_comments.py b/.github/scripts/check_reference_comments.py new file mode 100644 index 0000000000..86055dcf01 --- /dev/null +++ b/.github/scripts/check_reference_comments.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Validate reference.conf comment coverage. + +Rules enforced: + 1. Every user-defined key line must have a comment. + 2. A key is documented by either an inline comment on the same line or a + comment on the immediately preceding line. Blank lines do not count. + 3. Object fields repeated across array elements are checked only on their + first occurrence within that array. + +Design scope — basic coverage gate, not a full HOCON parser +----------------------------------------------------------- +This script is deliberately line-oriented. pyhocon is not used because it +discards comments, and this gate only needs enough structure to track braces +and arrays. + +As a consequence, several HOCON constructs are handled in a simplified way. +Each known limitation is listed below together with its practical risk level +for reference.conf. The gate is intentionally kept simple: reference.conf +uses a small, stable subset of HOCON syntax, and the constructs below are +either forbidden by the project's config conventions or have never appeared +in the file. + +Known limitations (all rated LOW risk for reference.conf): + + A. Silent miss — keys matched by none of the patterns below are neither + checked nor flagged; they pass silently: + + * Quoted keys: "my-key" = value + KEY_LINE requires [A-Za-z_] at the start; a leading '"' never matches. + reference.conf uses only plain lowerCamelCase keys — risk: none. + + * Hyphenated keys: my-key = value + KEY_LINE allows only [A-Za-z0-9_]; '-' is excluded. + reference.conf has no hyphenated keys — risk: none. + + * Append operator: foo += bar + KEY_LINE ends with [:={]; '+' before '=' is not in that set. + reference.conf does not use '+=' — risk: none. + + * Inline-object sub-keys: outer = {inner = 1} + KEY_LINE.match() anchors to the line start, so only the first key on + each line ('outer') is detected; 'inner' inside the braces is missed. + reference.conf expands every block across multiple lines — risk: none. + + * Second key on a bare-value line: a = 1, b = 2 + re.match() matches only at the start; 'b' is invisible to KEY_LINE. + reference.conf never puts two assignments on one line — risk: none. + + B. False positive — non-key content incorrectly flagged as a missing key: + + * Triple-quoted multi-line strings (key = \"\"\" ... \"\"\") + strip_quoted() is line-oriented and does not track triple-quote spans + across lines. Lines inside the string body that look like 'word = ...' + are matched by KEY_LINE and reported as keys lacking comments. + reference.conf contains no triple-quoted strings — risk: none. + If triple-quoted strings are ever introduced, add a triple-quote span + tracker at the top of the collect_keys() loop (see inline comment there). + + C. False pass — a key with no real comment is incorrectly classified as + documented: + + * Block opened on the next line: key =\n{ + opening_after_key() only scans the current line for '{' or '['. + If the opening brace appears on the next line, no named frame is + pushed for the key, so array-element deduplication silently stops + working for that block's contents. + reference.conf always opens blocks on the same line as the key + (e.g. "genesis.block = {") — risk: none. + + * Bare URL value: key = http://example.com + has_inline_comment() sees '//' in the URL and returns True, treating + the URL as an inline comment. Quoting the URL ("http://...") avoids + this because strip_quoted() removes the string contents before the + comment scan. reference.conf contains no bare (unquoted) URLs and all + such values are either quoted or absent — risk: none. +""" +import re +import sys +from pathlib import Path + +KEY_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*[:={]") +COMMENT_LINE = re.compile(r"^\s*(#|//)") + + +def strip_quoted(line): + """Remove quoted string contents while preserving comments and delimiters.""" + out = [] + quote = None + escaped = False + i = 0 + while i < len(line): + ch = line[i] + if quote: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == quote: + quote = None + out.append(ch) + i += 1 + continue + if ch in ('"', "'"): + quote = ch + out.append(ch) + i += 1 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def strip_comments(line): + """Strip # and // comments outside quotes.""" + text = strip_quoted(line) + i = 0 + while i < len(text): + ch = text[i] + if ch == "#": + return text[:i] + if ch == "/" and i + 1 < len(text) and text[i + 1] == "/": + return text[:i] + i += 1 + return text + + +def has_inline_comment(line): + text = strip_quoted(line) + i = 0 + while i < len(text): + if text[i] == "#": + return True + if text[i] == "/" and i + 1 < len(text) and text[i + 1] == "/": + return True + i += 1 + return False + + +def has_prevline_comment(lines, index): + if index == 0: + return False + prev = lines[index - 1] + return bool(prev.strip()) and bool(COMMENT_LINE.match(prev)) + + +def opening_after_key(code, match): + pos = match.end() - 1 + ch = code[pos] + if ch in "{[": + return ch, pos + if ch in ":=": + i = pos + 1 + while i < len(code) and code[i].isspace(): + i += 1 + if i < len(code) and code[i] in "{[": + return code[i], i + return None, None + + +def nearest_array_frame(stack): + for frame in reversed(stack): + if frame["type"] == "array": + return frame + return None + + +def pop_frame(stack, closer): + target_type = "object" if closer == "}" else "array" + while stack: + frame = stack.pop() + if frame["type"] == target_type: + return + + +def scan_structure(code, stack, key_open_pos=None): + i = 0 + while i < len(code): + ch = code[i] + if key_open_pos is not None and i == key_open_pos: + i += 1 + continue + if ch == "{": + stack.append({"type": "object", "name": None, "seen": set()}) + elif ch == "[": + stack.append({"type": "array", "name": None, "seen": set()}) + elif ch == "}": + pop_frame(stack, "}") + elif ch == "]": + pop_frame(stack, "]") + i += 1 + + +def collect_keys(path, list_all=False): + """Scan *path* line by line and classify every HOCON key. + + Returns + ------- + missing : list of (line_no, key) + Keys that lack a comment and are not exempt. Empty means the file + passes the gate. + seen_rows : list of (line_no, key, status) + One entry per matched key line, in file order. Populated only when + *list_all* is True (``--list`` flag); always empty otherwise. + status is one of: "commented" | "dedup" | "missing". + """ + lines = path.read_text().splitlines() + + # stack — bracket-nesting context, one frame per open { or [. + # Each frame is a dict: + # "type" : "object" | "array" + # "name" : str | None — the key that opened this block, or None for + # anonymous braces/brackets. + # "seen" : set — only meaningful on array frames: the set of + # key names already encountered inside this array. + # Enables deduplication so that repeated keys in + # homogeneous array elements (e.g. rate.limiter + # entries) are only checked on their first + # occurrence. + stack = [] + + # missing — accumulates (line_no, key) for every key that is neither + # exempt nor deduplicated yet has no comment. Drives the exit-1 path. + missing = [] + + # seen_rows — full audit log for --list mode: (line_no, key, status). + # Built only when list_all=True to avoid wasting memory in normal runs. + seen_rows = [] + + for index, raw in enumerate(lines): + line_no = index + 1 + + # code: raw line with comment text removed. Used for KEY_LINE + # matching and bracket counting so that "#" / "//" inside values + # do not confuse the structural parser. + code = strip_comments(raw) + + stripped = raw.lstrip() + is_comment = stripped.startswith("#") or stripped.startswith("//") + + # Skip pure comment lines; never treat them as key lines. + match = None if is_comment else KEY_LINE.match(code) + + key = None + status = "non-key" + key_open_pos = None # position in `code` of the { or [ that this key opens + if match: + key = match.group(1) + + # opener: "{" or "[" when the key introduces a block/array on + # the same line (e.g. "node {" or "active = ["). + # key_open_pos: char index of that opener inside `code`, passed + # to scan_structure so it is not counted a second time. + opener, key_open_pos = opening_after_key(code, match) + + # --- Array deduplication --- + # Find the innermost enclosing array frame (if any). Within an + # array, all elements share the same schema, so only the first + # occurrence of each key name needs a comment. + deduped = False + array_frame = nearest_array_frame(stack) + if array_frame is not None: + if key in array_frame["seen"]: + # Already checked on an earlier array element — skip. + deduped = True + else: + # First time we see this key in this array; record it and + # fall through to the normal comment check below. + array_frame["seen"].add(key) + + # --- Comment check --- + # A key is considered documented if it has an inline comment on + # the same line *or* a non-blank comment on the immediately + # preceding line (blank lines between comment and key do NOT + # count as "preceding"). + commented = has_inline_comment(raw) or has_prevline_comment(lines, index) + + # Assign the final status in priority order. + if deduped: + status = "dedup" + elif commented: + status = "commented" + else: + status = "missing" + missing.append((line_no, key)) + + # If this key opens a new block or array, push a fresh frame so + # that nested keys and future deduplication operate in the correct + # scope. We push *after* classifying the key itself so that the + # key is judged in its *parent* scope, not inside itself. + if opener: + stack.append({ + "type": "object" if opener == "{" else "array", + "name": key, + "seen": set(), + }) + + # Walk any remaining { } [ ] characters in `code` that were NOT the + # opener just pushed above. This keeps the stack in sync for lines + # that contain multiple brackets (e.g. closing braces after a value). + scan_structure(code, stack, key_open_pos) + + if list_all and match: + seen_rows.append((line_no, key, status)) + + return missing, seen_rows + + +def main(argv): + list_all = False + args = list(argv[1:]) + if "--list" in args: + list_all = True + args.remove("--list") + if len(args) != 1: + print(f"usage: {argv[0]} [--list] ", file=sys.stderr) + return 2 + + path = Path(args[0]) + if not path.is_file(): + print(f"error: file not found: {path}", file=sys.stderr) + return 2 + + missing, seen_rows = collect_keys(path, list_all) + + if list_all: + for line_no, key, status in seen_rows: + print(f"{line_no}: {key} [{status}]") + print() + + if missing: + lines_out = [ + f"Comment coverage violations ({len(missing)}) — each key " + "needs an inline or immediately preceding comment:" + ] + for line_no, key in missing: + lines_out.append(f" comment: line {line_no}: {key}") + print("\n".join(lines_out)) + print() + + entries = [f"line {line_no}: {key}" for line_no, key in missing] + body = ( + f"reference.conf has {len(missing)} comment coverage violation(s):%0A" + + "%0A".join(entries) + ) + print(f"::error file={path},title=reference.conf::{body}") + print( + f"FAIL: {len(missing)} comment coverage violation(s) in {path}", + file=sys.stderr, + ) + return 1 + + print(f"OK: {path} — all keys have comments") + 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 7ae169a869..fed00a2a97 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -119,6 +119,12 @@ jobs: python3 .github/scripts/check_reference_conf.py \ common/src/main/resources/reference.conf + - name: Validate reference.conf comment coverage + shell: bash + run: | + python3 .github/scripts/check_reference_comments.py \ + common/src/main/resources/reference.conf + - name: Set up JDK 17 uses: actions/setup-java@v5 with: diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 549e280bbe..68be6c2a94 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -38,16 +38,18 @@ # # ============================================================================= +# Network type placeholder; deprecated and has no effect. net { # type is deprecated and has no effect. # type = mainnet } +# Storage engine and database settings. storage { # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) db.engine = "LEVELDB" - db.sync = false - db.directory = "database" + db.sync = false # Whether to force synchronous database writes. + db.directory = "database" # Database directory under the node output path. # Whether to write transaction result in transactionRetStore transHistory.switch = "on" @@ -91,27 +93,27 @@ storage { # ] properties = [] - needToUpdateAsset = true + needToUpdateAsset = true # Whether to run legacy asset update logic. # RocksDB settings (only used when db.engine = "ROCKSDB") # Strongly recommend NOT modifying unless you know every item's meaning clearly. dbSettings = { - levelNumber = 7 + levelNumber = 7 // Number of RocksDB levels. compactThreads = 0 // 0 = auto: max(availableProcessors, 1) blocksize = 16 // n * KB maxBytesForLevelBase = 256 // n * MB - maxBytesForLevelMultiplier = 10 - level0FileNumCompactionTrigger = 2 + maxBytesForLevelMultiplier = 10 // Level size multiplier. + level0FileNumCompactionTrigger = 2 // L0 files that trigger compaction. targetFileSizeBase = 64 // n * MB - targetFileSizeMultiplier = 1 - maxOpenFiles = 5000 + targetFileSizeMultiplier = 1 // Target file size multiplier. + maxOpenFiles = 5000 // Maximum open files for RocksDB. } - balance.history.lookup = false + balance.history.lookup = false # Whether to enable historical balance lookup. # Checkpoint version for snapshot mechanism. Version 2 enables V2 snapshot. checkpoint.version = 1 - checkpoint.sync = true + checkpoint.sync = true # Whether to sync when write checkpoint storage. # Estimated number of block transactions (default 1000, min 100, max 10000). # Total cached transactions = 65536 * txCache.estimatedTransactions @@ -128,10 +130,11 @@ storage { # } } +# Node discovery settings. node.discovery = { - enable = false - persist = false - external.ip = "" + enable = false # Whether to enable node discovery. + persist = false # Whether to persist discovered peers. + external.ip = "" # External IP advertised to peers. } # Custom stop condition @@ -141,30 +144,31 @@ node.discovery = { # BlockCount = 12 # block sync count after node start # } -node.backup { +node.backup { # Backup node election settings. port = 10001 # UDP listen port; each member should have the same configuration priority = 0 # Node priority; each member should use a different priority keepAliveInterval = 3000 # Keep-alive interval (ms); each member should have the same configuration - members = [ + members = [ # Backup member IP list. # "ip", # Peer IP list, better to add at most one IP; must not contain this node's own IP ] } # Algorithm for generating public key from private key. Do not modify to avoid forks. crypto { - engine = "eckey" + engine = "eckey" # Signature engine. } # Energy limit block number (config key has typo "enery" preserved for backward compatibility) enery.limit.block.num = 4727890 -node.metrics = { - prometheus { - enable = false - port = 9527 +node.metrics = { # Node metrics settings. + prometheus { # Prometheus exporter settings. + enable = false # Whether to enable Prometheus metrics. + port = 9527 # Prometheus exporter port. } } +# Node runtime, networking, and API settings. node { # Trust node for solidity node (example: "127.0.0.1:50051"). trustNode = "" @@ -172,8 +176,8 @@ node { # Expose extension api to public or not walletExtensionApi = false - listen.port = 18888 - fetchBlock.timeout = 500 + listen.port = 18888 # P2P listen port. + fetchBlock.timeout = 500 # Block fetch timeout (ms). # Number of blocks to fetch in one batch during sync. Range: [100, 2000]. syncFetchBatchNum = 2000 @@ -184,12 +188,12 @@ node { # Number of validate sign threads, 0 = auto (availableProcessors) validateSignThreadNum = 0 - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - maxConnectionsWithSameIp = 2 - maxHttpConnectNumber = 50 - minParticipationRate = 0 + maxConnections = 30 # Maximum peer connections. + minConnections = 8 # Minimum peer connections to maintain. + minActiveConnections = 3 # Minimum active peer connections. + maxConnectionsWithSameIp = 2 # Maximum peer connections per IP. + maxHttpConnectNumber = 50 # Maximum HTTP connections. + minParticipationRate = 0 # Minimum SR participation rate. # WARNING: Some shielded transaction APIs require sending private keys as parameters. # Calling these APIs on untrusted or remote nodes may leak your private keys. @@ -212,56 +216,57 @@ node { # Max block inv hashes accepted per peer per second. Minimum: 1. maxBlockInvPerSecond = 10 + # In P2P service,whether any peer connection can be disconnected, when peers connection over maxConnections. isOpenFullTcpDisconnect = false inactiveThreshold = 600 // seconds - maxFastForwardNum = 4 + maxFastForwardNum = 4 # Maximum fast-forward peers. # Legacy alias `maxActiveNodesWithSameIp` is still accepted from user config # (see NodeConfig alias-fallback) but is intentionally NOT defaulted here — # shipping it in reference.conf would always mask the modern `maxConnectionsWithSameIp`. metricsEnable = false - p2p { + p2p { # P2P protocol version settings. version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 } - active = [ + active = [ # Peers to actively connect to. # Active establish connection in any case # "ip:port", # "ip:port" ] - passive = [ + passive = [ # Peers allowed to passively connect. # Passive accept connection in any case # "ip:port", # "ip:port" ] - fastForward = [ + fastForward = [ # Fast-forward peer list. "100.27.171.62:18888", "15.188.6.125:18888" ] - http { - fullNodeEnable = true - fullNodePort = 8090 - solidityEnable = true - solidityPort = 8091 - PBFTEnable = true - PBFTPort = 8092 + http { # HTTP API settings. + fullNodeEnable = true # Whether to enable FullNode HTTP API. + fullNodePort = 8090 # FullNode HTTP API port. + solidityEnable = true # Whether to enable Solidity HTTP API. + solidityPort = 8091 # Solidity HTTP API port. + PBFTEnable = true # Whether to enable PBFT HTTP API. + PBFTPort = 8092 # PBFT HTTP API port. # Maximum HTTP request body size (default 4M). Setting to 0 rejects all non-empty request bodies. # Independent from rpc.maxMessageSize. maxMessageSize = 4194304 } - rpc { - enable = true - port = 50051 - solidityEnable = true - solidityPort = 50061 - PBFTEnable = true - PBFTPort = 50071 + rpc { # gRPC API settings. + enable = true # Whether to enable FullNode gRPC API. + port = 50051 # FullNode gRPC API port. + solidityEnable = true # Whether to enable Solidity gRPC API. + solidityPort = 50061 # Solidity gRPC API port. + PBFTEnable = true # Whether to enable PBFT gRPC API. + PBFTPort = 50071 # PBFT gRPC API port. # Number of gRPC threads, 0 = auto (availableProcessors / 2) thread = 0 @@ -297,7 +302,7 @@ node { # Reflection service switch for grpcurl tool reflectionService = false - trxCacheEnable = false + trxCacheEnable = false # Whether to enable transaction cache in broadcast transaction API(rpc and http). } # Number of solidity threads in FullNode. @@ -323,18 +328,18 @@ node { # Dynamic loading configuration function dynamicConfig = { - enable = false - checkInterval = 600 + enable = false # Whether to enable dynamic config loading. + checkInterval = 600 # Dynamic config check interval (s). } # Block solidification check unsolidifiedBlockCheck = false - maxUnsolidifiedBlocks = 54 - blockCacheTimeout = 60 + maxUnsolidifiedBlocks = 54 # Maximum unsolidified blocks allowed,when accept transaction + blockCacheTimeout = 60 # Block cache timeout (s) in P2P service # TCP and transaction limits maxTransactionPendingSize = 2000 - pendingTransactionTimeout = 60000 + pendingTransactionTimeout = 60000 # Pending transaction timeout (ms). # total cached trx across handler queues + pending + rePush maxTrxCacheSize = 50000 @@ -343,12 +348,12 @@ node { # Shielded transaction (ZK) zenTokenId = "000000" - shieldedTransInPendingMaxCounts = 10 + shieldedTransInPendingMaxCounts = 10 # Max shielded transactions in pending pool. # Contract proto validation thread pool (0 = auto: availableProcessors) validContractProto.threads = 0 - dns { + dns { # DNS discovery and publish settings. # DNS URLs to discover peers, format: tree://{pubkey}@{domain}. Default: empty. treeUrls = [ # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", @@ -388,17 +393,17 @@ node { # Deprecated: these fields were used by the old connection-factor algorithm. # They are still accepted from user config for backward compatibility but have no effect. activeConnectFactor = 0.1 - connectFactor = 0.6 + connectFactor = 0.6 # Deprecated connection factor. - jsonrpc { + jsonrpc { # JSON-RPC API settings. # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, # you will not be able to get the data from eth_getLogs for that period of time. Default: false httpFullNodeEnable = false - httpFullNodePort = 8545 - httpSolidityEnable = false - httpSolidityPort = 8555 - httpPBFTEnable = false - httpPBFTPort = 8565 + httpFullNodePort = 8545 # FullNode JSON-RPC HTTP port. + httpSolidityEnable = false # Whether to enable Solidity JSON-RPC HTTP API. + httpSolidityPort = 8555 # Solidity JSON-RPC HTTP port. + httpPBFTEnable = false # Whether to enable PBFT JSON-RPC HTTP API. + httpPBFTPort = 8565 # PBFT JSON-RPC HTTP port. # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit maxBlockRange = 5000 @@ -476,7 +481,7 @@ rate.limiter = { # } ] - p2p = { + p2p = { # P2P message rate limits. # QPS ceiling for individual P2P message types received from peers. # Values are doubles; fractional QPS is allowed (e.g. 0.5 = one per 2 s). syncBlockChain = 3.0 # SyncBlockChain handshake messages @@ -494,8 +499,8 @@ rate.limiter = { apiNonBlocking = false } -seed.node = { - ip.list = [ +seed.node = { # Bootstrap seed node settings. + ip.list = [ # Seed node addresses. "3.225.171.164:18888", "52.8.46.215:18888", "3.79.71.167:18888", @@ -548,10 +553,10 @@ genesis.block = { # Blackhole – receives burned TRX; initialized to Long.MIN_VALUE so it can only increase assets = [ { - accountName = "Zion" - accountType = "AssetIssue" - address = "TLLM21wteSPs4hKjbxgmH1L6poyMjeTbHm" - balance = "99000000000000000" + accountName = "Zion" # human-readable label stored on-chain; must not be blank + accountType = "AssetIssue" # one of: Normal, AssetIssue, Contract + address = "TLLM21wteSPs4hKjbxgmH1L6poyMjeTbHm" # Base58Check-encoded account address (T...) + balance = "99000000000000000" # initial balance in SUN (1 TRX = 1,000,000 SUN); stored as String }, { accountName = "Sun" @@ -575,9 +580,9 @@ genesis.block = { # The 27 witnesses with the highest voteCount produce the first round of blocks. witnesses = [ { - address: THKJYuUmMKKARNf7s2VT51g5uPY6KEqnat, - url = "http://GR1.com", - voteCount = 100000026 + address: THKJYuUmMKKARNf7s2VT51g5uPY6KEqnat, # Base58Check-encoded SR address (T...) + url = "http://GR1.com", # SR's public URL (informational only, stored on-chain) + voteCount = 100000026 # initial vote count; seeds SR ranking before any user votes are cast }, { address: TVDmPWGYxgi5DNeW8hXrzrhY8Y6zgxPNg4, @@ -727,15 +732,15 @@ genesis.block = { # When it is empty,the localwitness is configured with the private key of the witness account. # localWitnessAccountAddress = -localwitness = [ +localwitness = [ # Local witness private keys. ] # localwitnesskeystore = [ # "localwitnesskeystore.json" # ] -block = { - needSyncCheck = false +block = { # Block processing settings. + needSyncCheck = false // Whether to check sync before producing blocks. maintenanceTimeInterval = 21600000 // 6 hours (ms) proposalExpireTime = 259200000 // 3 days (ms), controlled by committee proposal checkFrozenTime = 1 // maintenance periods to check frozen balance (test only) @@ -747,14 +752,14 @@ trx.reference.block = "solid" # Transaction expiration time in milliseconds. trx.expiration.timeInMilliseconds = 60000 -vm = { - supportConstant = false - maxEnergyLimitForConstant = 100000000 - minTimeRatio = 0.0 - maxTimeRatio = 5.0 - saveInternalTx = false - lruCacheSize = 500 - vmTrace = false +vm = { # TVM execution settings. + supportConstant = false # Whether to support constant contract calls. + maxEnergyLimitForConstant = 100000000 # Max energy for constant calls. + minTimeRatio = 0.0 # Minimum VM time ratio. + maxTimeRatio = 5.0 # Maximum VM time ratio. + saveInternalTx = false # Whether to save internal transactions. + lruCacheSize = 500 # VM LRU cache size. + vmTrace = false # Whether to enable VM trace output. # Whether to store featured internal transactions (freeze, vote, etc.) saveFeaturedInternalTx = false @@ -784,67 +789,69 @@ vm = { constantCallTimeoutMs = 0 } -# Governance proposal toggle parameters. All default to 0 (disabled). +# Governance parameters exposed by /wallet/getchainparameters. +# Comments list the API chainParameter key and ProposalType ID where applicable. +# All default to 0 (disabled) unless noted. # Controlled by on-chain committee proposals, not manual configuration. # Setting them in config is only for private chain testing. committee = { - allowCreationOfContracts = 0 - allowMultiSign = 0 - allowAdaptiveEnergy = 0 - allowDelegateResource = 0 - allowSameTokenName = 0 - allowTvmTransferTrc10 = 0 - allowTvmConstantinople = 0 - allowTvmSolidity059 = 0 - forbidTransferToContract = 0 - allowShieldedTRC20Transaction = 0 - allowTvmIstanbul = 0 - allowMarketTransaction = 0 - allowProtoFilterNum = 0 - allowAccountStateRoot = 0 - changedDelegation = 0 - allowPBFT = 0 - pBFTExpireNum = 20 - allowTransactionFeePool = 0 - allowBlackHoleOptimization = 0 - allowNewResourceModel = 0 - allowReceiptsMerkleRoot = 0 - allowTvmFreeze = 0 - allowTvmVote = 0 - unfreezeDelayDays = 0 - allowTvmLondon = 0 - allowTvmCompatibleEvm = 0 - allowHigherLimitForMaxCpuTimeOfOneTx = 0 - allowNewRewardAlgorithm = 0 - allowOptimizedReturnValueOfChainId = 0 - allowTvmShangHai = 0 - allowOldRewardOpt = 0 - allowEnergyAdjustment = 0 - allowStrictMath = 0 - consensusLogicOptimization = 0 - allowTvmCancun = 0 - allowTvmBlob = 0 - allowAccountAssetOptimization = 0 - allowAssetOptimization = 0 - allowNewReward = 0 - memoFee = 0 - allowDelegateOptimization = 0 - allowDynamicEnergy = 0 - dynamicEnergyThreshold = 0 - dynamicEnergyIncreaseFactor = 0 - dynamicEnergyMaxFactor = 0 + allowCreationOfContracts = 0 # getAllowCreationOfContracts, #9: enable smart contract creation + allowMultiSign = 0 # getAllowMultiSign, #20: enable account permission multi-signature + allowAdaptiveEnergy = 0 # getAllowAdaptiveEnergy, #21: enable adaptive energy limits + allowDelegateResource = 0 # getAllowDelegateResource, #16: enable delegated resource operations + allowSameTokenName = 0 # getAllowSameTokenName, #15: allow duplicate TRC10 token names + allowTvmTransferTrc10 = 0 # getAllowTvmTransferTrc10, #18: allow TRC10 transfer in TVM + allowTvmConstantinople = 0 # getAllowTvmConstantinople, #26: enable Constantinople TVM rules + allowTvmSolidity059 = 0 # getAllowTvmSolidity059, #32: enable Solidity 0.5.9 TVM rules + forbidTransferToContract = 0 # getForbidTransferToContract, #35: forbid direct transfers to contracts + allowShieldedTRC20Transaction = 0 # getAllowShieldedTRC20Transaction, #39: enable shielded TRC20 transfers + allowTvmIstanbul = 0 # getAllowTvmIstanbul, #41: enable Istanbul TVM rules + allowMarketTransaction = 0 # getAllowMarketTransaction, #44: enable market transactions + allowProtoFilterNum = 0 # getAllowProtoFilterNum, #24: enable protobuf field-number filtering + allowAccountStateRoot = 0 # getAllowAccountStateRoot, #25: enable account state root + changedDelegation = 0 # getChangeDelegation, #30: enable delegation changes + allowPBFT = 0 # getAllowPBFT, #40: enable PBFT consensus + pBFTExpireNum = 20 # PBFT message expiration, in maintenance rounds + allowTransactionFeePool = 0 # getAllowTransactionFeePool, #48: enable transaction fee pool + allowBlackHoleOptimization = 0 # getAllowOptimizeBlackHole, #49: enable blackhole account optimization + allowNewResourceModel = 0 # getAllowNewResourceModel, #51: enable new resource model + allowReceiptsMerkleRoot = 0 # receiptsMerkleRoot: enable receipts Merkle root + allowTvmFreeze = 0 # getAllowTvmFreeze, #52: enable freeze operations in TVM + allowTvmVote = 0 # getAllowTvmVote, #59: enable vote operations in TVM + unfreezeDelayDays = 0 # getUnfreezeDelayDays, #70: resource unfreeze delay days [1, 365] + allowTvmLondon = 0 # getAllowTvmLondon, #63: enable London TVM rules + allowTvmCompatibleEvm = 0 # getAllowTvmCompatibleEvm, #60: enable EVM-compatible TVM behavior + allowHigherLimitForMaxCpuTimeOfOneTx = 0 # getAllowHigherLimitForMaxCpuTimeOfOneTx, #65: allow higher tx CPU limit + allowNewRewardAlgorithm = 0 # newRewardAlgorithm: enable new reward algorithm + allowOptimizedReturnValueOfChainId = 0 # getAllowOptimizedReturnValueOfChainId, #71: optimize CHAINID return value + allowTvmShangHai = 0 # getAllowTvmShangHai, #76: enable Shanghai TVM rules + allowOldRewardOpt = 0 # getAllowOldRewardOpt, #79: enable old reward optimization + allowEnergyAdjustment = 0 # getAllowEnergyAdjustment, #81: enable energy adjustment + allowStrictMath = 0 # getAllowStrictMath, #87: enable strict arithmetic checks + consensusLogicOptimization = 0 # getConsensusLogicOptimization, #88: enable consensus logic optimization + allowTvmCancun = 0 # getAllowTvmCancun, #83: enable Cancun TVM rules + allowTvmBlob = 0 # getAllowTvmBlob, #89: enable blob transaction support in TVM + allowAccountAssetOptimization = 0 # getAllowAccountAssetOptimization, #53: enable account asset optimization + allowAssetOptimization = 0 # getAllowAssetOptimization, #66: enable asset optimization + allowNewReward = 0 # getAllowNewReward, #67: enable new reward logic + memoFee = 0 # getMemoFee, #68: memo fee in SUN [0, 1000000000] + allowDelegateOptimization = 0 # getAllowDelegateOptimization, #69: enable delegate optimization + allowDynamicEnergy = 0 # getAllowDynamicEnergy, #72: enable contract dynamic energy model + dynamicEnergyThreshold = 0 # getDynamicEnergyThreshold, #73: usage threshold for dynamic energy + dynamicEnergyIncreaseFactor = 0 # getDynamicEnergyIncreaseFactor, #74: dynamic energy increase factor + dynamicEnergyMaxFactor = 0 # getDynamicEnergyMaxFactor, #75: maximum dynamic energy factor } -event.subscribe = { - enable = false +event.subscribe = { # Event subscription settings. + enable = false # Whether to enable event subscription. - native = { + native = { # Native event queue settings. useNativeQueue = false // if true, use native message queue, else use event plugin. bindport = 5555 // bind port sendqueuelength = 1000 // max length of send queue } - version = 0 + version = 0 # Event subscription version. # Specify the starting block number to sync historical events. Only applicable when version = 1. # After performing a full event sync, set this value to 0 or a negative number. startSyncBlockNum = 0 @@ -853,12 +860,12 @@ event.subscribe = { # dbname|username|password. To auto-create indexes on missing collections, append |2: # dbname|username|password|2 (if collection exists, indexes must be created manually). dbconfig = "" - contractParse = true + contractParse = true # Whether to parse contract event data. - topics = [ + topics = [ # Event trigger topics. { triggerName = "block" // block trigger, the value can't be modified - enable = false + enable = false // Whether to enable this trigger. topic = "block" // plugin topic, the value could be modified solidified = false // if set true, just need solidified block. Default: false }, @@ -867,7 +874,9 @@ event.subscribe = { enable = false topic = "transaction" solidified = false - ethCompatible = false // if set true, add transactionIndex, cumulativeEnergyUsed, preCumulativeLogCount, logList, energyUnitPrice. Default: false + // if set true, add transactionIndex, cumulativeEnergyUsed, preCumulativeLogCount, logList, energyUnitPrice. + // Default: false + ethCompatible = false }, { triggerName = "contractevent" // contractevent represents contractlog data decoded by the ABI. @@ -898,13 +907,13 @@ event.subscribe = { } ] - filter = { + filter = { # Event filter settings. fromblock = "" // "", "earliest", or a specific block number as the beginning of the queried range toblock = "" // "", "latest", or a specific block number as end of the queried range - contractAddress = [ + contractAddress = [ // Contract addresses to subscribe; "" means any contract address. "" // contract address to subscribe; "" means any contract address ] - contractTopic = [ + contractTopic = [ // Contract topics to subscribe; "" means any contract topic. "" // contract topic to subscribe; "" means any contract topic ] } From a771d440d9f768a80f64de968ae9a15dfc03fc8b Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Tue, 2 Jun 2026 18:16:51 +0800 Subject: [PATCH 02/13] fix(db): fix storage config properties (#6806) --- .../main/java/org/tron/core/config/README.md | 7 +- .../org/tron/core/config/args/Storage.java | 33 +------ .../tron/core/config/args/StorageConfig.java | 96 +++++++++++-------- common/src/main/resources/reference.conf | 49 +++++++--- .../core/config/args/StorageConfigTest.java | 92 ++++++++++++++++++ .../tron/core/config/args/StorageTest.java | 50 +++++----- framework/src/test/resources/config-test.conf | 7 +- 7 files changed, 215 insertions(+), 119 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/README.md b/common/src/main/java/org/tron/core/config/README.md index c34994519d..1380c98984 100644 --- a/common/src/main/java/org/tron/core/config/README.md +++ b/common/src/main/java/org/tron/core/config/README.md @@ -28,10 +28,7 @@ storage { { name = "account", path = "/path/to/accout", // relative or absolute path - createIfMissing = true, - paranoidChecks = true, - verifyChecksums = true, - compressionType = 1, // 0 - no compression, 1 - compressed with snappy + # following are only used for LevelDB blockSize = 4096, // 4 KB = 4 * 1024 B writeBufferSize = 10485760, // 10 MB = 10 * 1024 * 1024 B cacheSize = 10485760, // 10 MB = 10 * 1024 * 1024 B @@ -43,7 +40,7 @@ storage { ``` -As shown in the example above, the `accout` database will be stored in the path of `/path/to/accout/database` while the index be stored in `/path/to/accout/index`. And, the example also shows our default value of LevelDB options(Start from `createIfMissing` and end at `maxOpenFiles`). Please refer to the docs of [LevelDB](https://github.com/google/leveldb/blob/master/doc/index.md#performance) to figure out the details of these options. +As shown in the example above, the `accout` database will be stored in the path of `/path/to/accout/database` while the index be stored in `/path/to/accout/index`. And, the example also shows our default value of LevelDB options(Start from `blockSize` and end at `maxOpenFiles`). Please refer to the docs of [LevelDB](https://github.com/google/leveldb/blob/master/doc/index.md#performance) to figure out the details of these options. ## gRPC 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 f1317e0491..16dd8295be 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 @@ -25,7 +25,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.iq80.leveldb.CompressionType; import org.iq80.leveldb.Options; import org.tron.common.cache.CacheStrategies; import org.tron.common.cache.CacheType; @@ -170,26 +169,13 @@ private Property createPropertyFromBean(StorageConfig.PropertyConfig pc) { } Options dbOptions = newDefaultDbOptions(property.getName()); - applyPropertyOptions(pc, dbOptions); + // PropertyConfig is-a DbOptionOverride: apply only user-specified (non-null) overrides + // so unset fields keep the per-tier defaults already applied by newDefaultDbOptions. + applyDbOptionOverride(pc, dbOptions); property.setDbOptions(dbOptions); return property; } - /** - * Apply LevelDB options from PropertyConfig bean values. - */ - private static void applyPropertyOptions(StorageConfig.PropertyConfig pc, Options dbOptions) { - dbOptions.createIfMissing(pc.isCreateIfMissing()); - dbOptions.paranoidChecks(pc.isParanoidChecks()); - dbOptions.verifyChecksums(pc.isVerifyChecksums()); - dbOptions.compressionType( - CompressionType.getCompressionTypeByPersistentId(pc.getCompressionType())); - dbOptions.blockSize(pc.getBlockSize()); - dbOptions.writeBufferSize(pc.getWriteBufferSize()); - dbOptions.cacheSize(pc.getCacheSize()); - dbOptions.maxOpenFiles(pc.getMaxOpenFiles()); - } - /** * Set propertyMap from StorageConfig bean list. No Config parameter needed. */ @@ -247,19 +233,6 @@ public Options newDefaultDbOptions(String name) { // Apply only user-specified overrides (non-null fields) to LevelDB Options. private static void applyDbOptionOverride( StorageConfig.DbOptionOverride o, Options dbOptions) { - if (o.getCreateIfMissing() != null) { - dbOptions.createIfMissing(o.getCreateIfMissing()); - } - if (o.getParanoidChecks() != null) { - dbOptions.paranoidChecks(o.getParanoidChecks()); - } - if (o.getVerifyChecksums() != null) { - dbOptions.verifyChecksums(o.getVerifyChecksums()); - } - if (o.getCompressionType() != null) { - dbOptions.compressionType( - CompressionType.getCompressionTypeByPersistentId(o.getCompressionType())); - } if (o.getBlockSize() != null) { dbOptions.blockSize(o.getBlockSize()); } 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 e8823d8198..2c6c3e60a4 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 @@ -28,6 +28,8 @@ public class StorageConfig { private CheckpointConfig checkpoint = new CheckpointConfig(); private SnapshotConfig snapshot = new SnapshotConfig(); private TxCacheConfig txCache = new TxCacheConfig(); + // ConfigBeanFactory requires all bean fields present per item, so we parse manually. + @Setter(lombok.AccessLevel.NONE) private List properties = new ArrayList<>(); // merkleRoot is a nested object (e.g. { reward-vi = "hash..." }) not a string. @@ -54,6 +56,7 @@ public class StorageConfig { @Getter @Setter public static class DbConfig { + private String engine = "LEVELDB"; private boolean sync = false; private String directory = "database"; @@ -62,6 +65,7 @@ public static class DbConfig { @Getter @Setter public static class TransHistoryConfig { + // "switch" is a reserved Java keyword; ConfigBeanFactory calls setSwitch() which works fine @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) @@ -79,6 +83,7 @@ public void setSwitch(String v) { @Getter @Setter public static class DbSettingsConfig { + private int levelNumber = 7; private int compactThreads = 0; // 0 = auto: max(availableProcessors, 1) private int blocksize = 16; @@ -100,11 +105,13 @@ void postProcess() { @Getter @Setter public static class BalanceConfig { + private HistoryConfig history = new HistoryConfig(); @Getter @Setter public static class HistoryConfig { + private boolean lookup = false; } } @@ -112,6 +119,7 @@ public static class HistoryConfig { @Getter @Setter public static class CheckpointConfig { + private int version = 1; private boolean sync = true; } @@ -119,6 +127,7 @@ public static class CheckpointConfig { @Getter @Setter public static class SnapshotConfig { + private int maxFlushCount = 1; // Reject out-of-range values. Mirrors develop Storage.getSnapshotMaxFlushCountFromConfig. @@ -135,6 +144,7 @@ void postProcess() { @Getter @Setter public static class TxCacheConfig { + private int estimatedTransactions = 1000; private boolean initOptimization = false; @@ -148,19 +158,14 @@ void postProcess() { } } + // A named database entry: name/path plus the optional LevelDB option overrides + // inherited from DbOptionOverride (boxed types, null = "inherit per-tier defaults"). @Getter @Setter - public static class PropertyConfig { + public static class PropertyConfig extends DbOptionOverride { + private String name = ""; private String path = ""; - private boolean createIfMissing = true; - private boolean paranoidChecks = true; - private boolean verifyChecksums = true; - private int compressionType = 1; - private int blockSize = 4096; - private int writeBufferSize = 10485760; - private long cacheSize = 10485760; - private int maxOpenFiles = 100; } // Defaults come from reference.conf (loaded globally via Configuration.java) @@ -170,6 +175,7 @@ public static StorageConfig fromConfig(Config config) { StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); sc.rawStorageConfig = section; + sc.properties = readProperties(section); // Read optional LevelDB option overrides (default, defaultM, defaultL). sc.defaultDbOption = readDbOption(section, "default"); @@ -187,45 +193,17 @@ public static StorageConfig fromConfig(Config config) { @Getter @Setter public static class DbOptionOverride { - private Boolean createIfMissing; - private Boolean paranoidChecks; - private Boolean verifyChecksums; - private Integer compressionType; + private Integer blockSize; private Integer writeBufferSize; private Long cacheSize; private Integer maxOpenFiles; } - // Read optional LevelDB option override (default/defaultM/defaultL). - // Not bean-bound: users may only set a subset of keys (e.g. just maxOpenFiles), - // ConfigBeanFactory requires all fields present so partial overrides would fail. - private static DbOptionOverride readDbOption(Config section, String key) { - if (!section.hasPath(key)) { - return null; - } - ConfigObject conf = section.getObject(key); - DbOptionOverride o = new DbOptionOverride(); - if (conf.containsKey("createIfMissing")) { - o.setCreateIfMissing( - Boolean.parseBoolean(conf.get("createIfMissing").unwrapped().toString())); - } - if (conf.containsKey("paranoidChecks")) { - o.setParanoidChecks( - Boolean.parseBoolean(conf.get("paranoidChecks").unwrapped().toString())); - } - if (conf.containsKey("verifyChecksums")) { - o.setVerifyChecksums( - Boolean.parseBoolean(conf.get("verifyChecksums").unwrapped().toString())); - } - if (conf.containsKey("compressionType")) { - String param = conf.get("compressionType").unwrapped().toString(); - try { - o.setCompressionType(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException("compressionType", Integer.class, param); - } - } + // Shared LevelDB option parser used by both readDbOption and readProperties. + // Fills the given target (boxed fields, null means "not specified by user") so the + // same parser can populate a plain DbOptionOverride or a PropertyConfig (which extends it). + private static void readLevelDbOptions(ConfigObject conf, DbOptionOverride o) { if (conf.containsKey("blockSize")) { String param = conf.get("blockSize").unwrapped().toString(); try { @@ -258,9 +236,43 @@ private static DbOptionOverride readDbOption(Config section, String key) { throwIllegalArgumentException("maxOpenFiles", Integer.class, param); } } + } + + // Read optional LevelDB option override for default/defaultM/defaultL keys. + private static DbOptionOverride readDbOption(Config section, String key) { + if (!section.hasPath(key)) { + return null; + } + DbOptionOverride o = new DbOptionOverride(); + readLevelDbOptions(section.getObject(key), o); return o; } + // Parse storage.properties list manually: ConfigBeanFactory requires every bean field to be + // present in each list item, but name+path-only entries (all LevelDB opts commented out) are + // valid — missing fields fall back to PropertyConfig Java defaults. + private static List readProperties(Config section) { + if (!section.hasPath("properties")) { + return new ArrayList<>(); + } + List items = section.getObjectList("properties"); + List result = new ArrayList<>(items.size()); + for (ConfigObject obj : items) { + PropertyConfig p = new PropertyConfig(); + if (obj.containsKey("name")) { + p.setName(obj.get("name").unwrapped().toString()); + } + if (obj.containsKey("path")) { + p.setPath(obj.get("path").unwrapped().toString()); + } + // Boxed nullable fields: unset options stay null so they inherit the per-tier + // defaults applied by newDefaultDbOptions instead of resetting them. + readLevelDbOptions(obj, p); + result.add(p); + } + return result; + } + private static void throwIllegalArgumentException(String param, Class type, String actual) { throw new IllegalArgumentException( String.format("[storage.properties] %s must be %s type, actual: %s.", diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 549e280bbe..be3fefb264 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -44,49 +44,68 @@ net { } storage { - # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) + # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB), case-insensitive db.engine = "LEVELDB" + + # Controls the database write strategy. + # true - Synchronous writes. Higher durability, lower performance. + # false - Asynchronous writes. Higher performance, but recent writes may be + # lost if the machine crashes before data is flushed to disk. + # Asynchronous writes can significantly improve FullNode block sync performance. db.sync = false + db.directory = "database" # Whether to write transaction result in transactionRetStore transHistory.switch = "on" - # Per-database LevelDB option overrides. Default: empty (all databases use global defaults). - # setting can improve leveldb performance .... start, deprecated for arm - # node: if this will increase process fds, you may check your ulimit if 'too many open files' error occurs - # see https://github.com/tronprotocol/tips/blob/master/tip-343.md for detail - # if you find block sync has lower performance, you can try this settings + # Per-database LevelDB option overrides.Default: empty. All databases use global LevelDB settings. + # These settings can be tuned to improve database performance during block synchronization. + # Note: + # - Increasing `maxOpenFiles` may significantly increase file descriptor usage. + # - If "Too many open files" errors occur, check the system `ulimit` configuration. + # - See TIP-343 for tuning recommendations: + # https://github.com/tronprotocol/tips/blob/master/tip-343.md + # The following presets are provided as default. If block synchronization + # performance is unsatisfactory, consider adjusting the settings accordingly. + # + # Global default settings: # default = { + # blockSize = 4096, // 4 KB + # writeBufferSize = 16777216, // 16 MB + # cacheSize = 33554432, // 32 MB # maxOpenFiles = 100 # } + # Default for bulk-read databases: code, contract # defaultM = { - # maxOpenFiles = 500 + # blockSize = 4096, // 4 KB + # writeBufferSize = 67108864, // 64 MB + # cacheSize = 33554432, // 32 MB + # maxOpenFiles = 100 // recommend 500 for production # } + # Default for frequently accessed databases: account, delegation, storage-row # defaultL = { - # maxOpenFiles = 1000 + # blockSize = 4096, // 4 KB + # writeBufferSize = 67108864, // 64 MB + # cacheSize = 33554432, // 32 MB + # maxOpenFiles = 100 // recommend 1000 for production # } - # setting can improve leveldb performance .... end, deprecated for arm # Per-database storage configuration overrides. Otherwise databases use global defaults and store # data in "output-directory" or the directory specified by the "-d" / "--output-directory" option. # Attention: name is a required field that must be set! # The name and path properties take effect for both LevelDB and RocksDB storage engines, - # while additional properties (createIfMissing, paranoidChecks, compressionType, etc.) + # while additional 4 properties (blockSize, writeBufferSize, cacheSize, maxOpenFiles) # only take effect when using LevelDB. # Example: # properties = [ # { # name = "account", # path = "storage_directory_test", - # createIfMissing = true, // deprecated for arm start - # paranoidChecks = true, - # verifyChecksums = true, - # compressionType = 1, // compressed with snappy # blockSize = 4096, // 4 KB = 4 * 1024 B # writeBufferSize = 10485760, // 10 MB = 10 * 1024 * 1024 B # cacheSize = 10485760, // 10 MB = 10 * 1024 * 1024 B - # maxOpenFiles = 100 // deprecated for arm end + # maxOpenFiles = 100 # }, # ] properties = [] diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index d8700880cd..e3f1925a76 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -2,12 +2,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import java.util.List; import org.junit.Test; import org.tron.common.math.StrictMathWrapper; +import org.tron.core.config.args.StorageConfig.PropertyConfig; public class StorageConfigTest { @@ -134,4 +137,93 @@ public void testTxCacheEstimatedWithinRangePreserved() { withRef("storage.txCache.estimatedTransactions = 5000")); assertEquals(5000, sc.getTxCache().getEstimatedTransactions()); } + + // ---- readProperties() ---- + + private static List props(String storageProperties) { + return StorageConfig.fromConfig(withRef(storageProperties)).getProperties(); + } + + @Test + public void testPropertiesDefaultEmpty() { + // reference.conf sets storage.properties = [] + assertTrue(StorageConfig.fromConfig(withRef()).getProperties().isEmpty()); + assertTrue(props("storage.properties = []").isEmpty()); + } + + @Test + public void testPropertiesNameAndPathOnly() { + // All LevelDB options omitted: name/path set, the four boxed fields stay null so + // they inherit the per-tier defaults applied later by newDefaultDbOptions. + List list = props( + "storage.properties = [ { name = account, path = some_path } ]"); + assertEquals(1, list.size()); + PropertyConfig p = list.get(0); + assertEquals("account", p.getName()); + assertEquals("some_path", p.getPath()); + assertNull(p.getBlockSize()); + assertNull(p.getWriteBufferSize()); + assertNull(p.getCacheSize()); + assertNull(p.getMaxOpenFiles()); + } + + @Test + public void testPropertiesNameOnlyKeepsEmptyPath() { + PropertyConfig p = props("storage.properties = [ { name = account } ]").get(0); + assertEquals("account", p.getName()); + assertEquals("", p.getPath()); + } + + @Test + public void testPropertiesFullOverrideParsed() { + PropertyConfig p = props( + "storage.properties = [ { name = foo, path = bar," + + " blockSize = 2, writeBufferSize = 3, cacheSize = 4, maxOpenFiles = 5 } ]").get(0); + assertEquals(Integer.valueOf(2), p.getBlockSize()); + assertEquals(Integer.valueOf(3), p.getWriteBufferSize()); + assertEquals(Long.valueOf(4L), p.getCacheSize()); + assertEquals(Integer.valueOf(5), p.getMaxOpenFiles()); + } + + @Test + public void testPropertiesPartialOverrideLeavesOthersNull() { + // Only blockSize is set; the other three stay null (inherit defaults). + PropertyConfig p = props( + "storage.properties = [ { name = foo, path = bar, blockSize = 8192 } ]").get(0); + assertEquals(Integer.valueOf(8192), p.getBlockSize()); + assertNull(p.getWriteBufferSize()); + assertNull(p.getCacheSize()); + assertNull(p.getMaxOpenFiles()); + } + + @Test + public void testPropertiesMultipleEntriesInOrder() { + List list = props( + "storage.properties = [" + + " { name = first, path = p1 }," + + " { name = second, path = p2, maxOpenFiles = 7 } ]"); + assertEquals(2, list.size()); + assertEquals("first", list.get(0).getName()); + assertNull(list.get(0).getMaxOpenFiles()); + assertEquals("second", list.get(1).getName()); + assertEquals(Integer.valueOf(7), list.get(1).getMaxOpenFiles()); + } + + @Test + public void testPropertiesMissingNameKeepsEmpty() { + // readProperties does not require name (validation is deferred to Storage); name stays "". + PropertyConfig p = props("storage.properties = [ { path = bar } ]").get(0); + assertEquals("", p.getName()); + assertEquals("bar", p.getPath()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPropertiesInvalidIntegerRejected() { + props("storage.properties = [ { name = foo, blockSize = not_a_number } ]"); + } + + @Test(expected = IllegalArgumentException.class) + public void testPropertiesInvalidLongRejected() { + props("storage.properties = [ { name = foo, cacheSize = not_a_number } ]"); + } } diff --git a/framework/src/test/java/org/tron/core/config/args/StorageTest.java b/framework/src/test/java/org/tron/core/config/args/StorageTest.java index 3c00c6ea00..c6b954838c 100644 --- a/framework/src/test/java/org/tron/core/config/args/StorageTest.java +++ b/framework/src/test/java/org/tron/core/config/args/StorageTest.java @@ -18,7 +18,6 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import java.io.File; -import org.iq80.leveldb.CompressionType; import org.iq80.leveldb.Options; import org.junit.AfterClass; import org.junit.Assert; @@ -44,17 +43,16 @@ private static void setupStorage() { + "storage.defaultL.maxOpenFiles = 1000\n" + "storage.properties = [\n" + " { name = account, path = storage_directory_test,\n" - + " createIfMissing = true, paranoidChecks = true, verifyChecksums = true,\n" - + " compressionType = 1, blockSize = 4096,\n" - + " writeBufferSize = 10485760, cacheSize = 10485760, maxOpenFiles = 100 },\n" + + " blockSize = 4096, writeBufferSize = 10485760, cacheSize = 10485760,\n" + + " maxOpenFiles = 100 },\n" + " { name = \"account-index\", path = storage_directory_test,\n" - + " createIfMissing = true, paranoidChecks = true, verifyChecksums = true,\n" - + " compressionType = 1, blockSize = 4096,\n" - + " writeBufferSize = 10485760, cacheSize = 10485760, maxOpenFiles = 100 },\n" + + " blockSize = 4096, writeBufferSize = 10485760, cacheSize = 10485760,\n" + + " maxOpenFiles = 100 },\n" + " { name = test_name, path = test_path,\n" - + " createIfMissing = false, paranoidChecks = false, verifyChecksums = false,\n" - + " compressionType = 1, blockSize = 2,\n" - + " writeBufferSize = 3, cacheSize = 4, maxOpenFiles = 5 }\n" + + " blockSize = 2, writeBufferSize = 3, cacheSize = 4, maxOpenFiles = 5 },\n" + // name/path-only entries: LevelDB options omitted, must inherit per-tier defaults + + " { name = delegation, path = test_path },\n" + + " { name = code, path = test_path }\n" + "]" ).withFallback(ConfigFactory.load(TestConstants.TEST_CONF)); StorageConfig sc = StorageConfig.fromConfig(cfg); @@ -83,30 +81,18 @@ public void getPath() { @Test public void getOptions() { Options options = StorageUtils.getOptionsByDbName("account"); - Assert.assertTrue(options.createIfMissing()); - Assert.assertTrue(options.paranoidChecks()); - Assert.assertTrue(options.verifyChecksums()); - Assert.assertEquals(CompressionType.SNAPPY, options.compressionType()); Assert.assertEquals(4096, options.blockSize()); Assert.assertEquals(10485760, options.writeBufferSize()); Assert.assertEquals(10485760L, options.cacheSize()); Assert.assertEquals(100, options.maxOpenFiles()); options = StorageUtils.getOptionsByDbName("test_name"); - Assert.assertFalse(options.createIfMissing()); - Assert.assertFalse(options.paranoidChecks()); - Assert.assertFalse(options.verifyChecksums()); - Assert.assertEquals(CompressionType.SNAPPY, options.compressionType()); Assert.assertEquals(2, options.blockSize()); Assert.assertEquals(3, options.writeBufferSize()); Assert.assertEquals(4L, options.cacheSize()); Assert.assertEquals(5, options.maxOpenFiles()); options = StorageUtils.getOptionsByDbName("some_name_not_exists"); - Assert.assertTrue(options.createIfMissing()); - Assert.assertTrue(options.paranoidChecks()); - Assert.assertTrue(options.verifyChecksums()); - Assert.assertEquals(CompressionType.SNAPPY, options.compressionType()); Assert.assertEquals(4 * 1024, options.blockSize()); Assert.assertEquals(16 * 1024 * 1024, options.writeBufferSize()); Assert.assertEquals(32 * 1024 * 1024L, options.cacheSize()); @@ -125,4 +111,24 @@ public void getOptions() { Assert.assertEquals(50, options.maxOpenFiles()); } + /** + * A properties entry that only sets name/path (all LevelDB options omitted) must inherit + * the per-tier defaults from newDefaultDbOptions instead of resetting them to the + * PropertyConfig defaults. Both "delegation" (DB_L) and "code" (DB_M) are listed with + * name/path only, so they must keep their tier writeBufferSize/maxOpenFiles. + */ + @Test + public void nameAndPathOnlyInheritsTierDefaults() { + Options ldb = StorageUtils.getOptionsByDbName("delegation"); + Assert.assertEquals(64 * 1024 * 1024, ldb.writeBufferSize()); + Assert.assertEquals(1000, ldb.maxOpenFiles()); + // unset cacheSize/blockSize inherit the base defaults, not PropertyConfig's old 10 MB + Assert.assertEquals(32 * 1024 * 1024L, ldb.cacheSize()); + Assert.assertEquals(4 * 1024, ldb.blockSize()); + + Options mdb = StorageUtils.getOptionsByDbName("code"); + Assert.assertEquals(64 * 1024 * 1024, mdb.writeBufferSize()); + Assert.assertEquals(500, mdb.maxOpenFiles()); + } + } diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 2277346234..a7bf77654c 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -23,14 +23,11 @@ storage { # { # name = "account", # path = "storage_directory_test", - # createIfMissing = true, // deprecated for arm start - # paranoidChecks = true, - # verifyChecksums = true, - # compressionType = 1, // compressed with snappy + # # following are only used for LevelDB # blockSize = 4096, // 4 KB = 4 * 1024 B # writeBufferSize = 10485760, // 10 MB = 10 * 1024 * 1024 B # cacheSize = 10485760, // 10 MB = 10 * 1024 * 1024 B - # maxOpenFiles = 100 // deprecated for arm end + # maxOpenFiles = 100 # }, ] From a41321ca15f3a74d71a4937719e93cf043200703 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Wed, 3 Jun 2026 13:48:26 +0800 Subject: [PATCH 03/13] fix(net): exit solidity node block sync on shutdown (#6804) --- README.md | 2 +- common/src/main/resources/reference.conf | 11 - .../org/tron/core/net/TronNetDelegate.java | 4 +- .../java/org/tron/program/SolidityNode.java | 8 +- .../core/zksnark/ShieldedReceiveTest.java | 279 ++++++++++-------- .../org/tron/program/SolidityNodeTest.java | 42 +++ 6 files changed, 209 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 575409b3a9..be84b44150 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ The TRON network is mainly divided into: - **Private Networks** Customized TRON networks set up by private entities for testing, development, or specific use cases. -Network selection is performed by specifying the appropriate configuration file upon full-node startup. Mainnet configuration: [config.conf](framework/src/main/resources/config.conf); Nile testnet configuration: [config-nile.conf](https://github.com/tron-nile-testnet/nile-testnet/blob/master/framework/src/main/resources/config-nile.conf) +Network selection is performed by specifying the appropriate configuration file upon full-node startup. Built-in configuration template: [reference.conf](common/src/main/resources/reference.conf); Mainnet configuration: [config.conf](framework/src/main/resources/config.conf); Nile testnet configuration: [config-nile.conf](https://github.com/tron-nile-testnet/nile-testnet/blob/master/framework/src/main/resources/config-nile.conf) ### 1. Join the TRON main network Launch a main-network full node with the built-in default configuration: diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index be3fefb264..8b69f6ef91 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -25,17 +25,6 @@ # Key naming rules (required for ConfigBeanFactory auto-binding): # - Use standard camelCase: maxConnections, syncFetchBatchNum, etc. # -# Keys that cannot auto-bind (handled via normalizeNonStandardKeys() or manual reads): -# -# 1. committee.pBFTExpireNum / committee.allowPBFT — normalized to camelCase in -# CommitteeConfig.normalizeNonStandardKeys() before ConfigBeanFactory binding. -# -# 2. node.isOpenFullTcpDisconnect — normalized to "openFullTcpDisconnect" in -# NodeConfig.normalizeNonStandardKeys() before ConfigBeanFactory binding. -# -# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — optional PascalCase nested keys; -# read manually in NodeConfig.fromConfig() after ConfigBeanFactory binding. -# # ============================================================================= net { diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 5f1540b672..23050f5218 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -111,7 +111,9 @@ public class TronNetDelegate { @PostConstruct public void init() { hitThread = new Thread(() -> { - LockSupport.park(); + while (!hitDown && !Thread.currentThread().isInterrupted()) { + LockSupport.park(); + } // to Guarantee Some other thread invokes unpark with the current thread as the target if (hitDown && exit) { System.exit(0); diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java index 9dbe92fb78..beb9ede2e1 100644 --- a/framework/src/main/java/org/tron/program/SolidityNode.java +++ b/framework/src/main/java/org/tron/program/SolidityNode.java @@ -122,7 +122,7 @@ private void getBlock() { logger.info("getBlock interrupted, exiting."); return; } catch (Exception e) { - if (!flag) { + if (!flag || tronNetDelegate.isHitDown()) { logger.info("getBlock stopped during shutdown, last block: {}.", blockNum); return; } @@ -185,6 +185,10 @@ private Block getBlockByNum(long blockNum) { sleep(exceptionSleepTime); } } catch (Exception e) { + if (!flag || tronNetDelegate.isHitDown()) { + logger.info("getBlockByNum stopped during shutdown, block: {}.", blockNum); + break; + } logger.error("Failed to get block: {}, reason: {}.", blockNum, e.getMessage()); sleep(exceptionSleepTime); } @@ -202,7 +206,7 @@ private long getLastSolidityBlockNum() { blockNum, remoteBlockNum, System.currentTimeMillis() - time); return blockNum; } catch (Exception e) { - if (!flag) { + if (!flag || tronNetDelegate.isHitDown()) { logger.info("getLastSolidityBlockNum stopped during shutdown."); return 0; } diff --git a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java index 0d14d6fbc2..5854b731e9 100755 --- a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java @@ -8,6 +8,7 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import java.lang.reflect.Field; import java.security.SignatureException; import java.util.Arrays; import java.util.HashSet; @@ -45,6 +46,8 @@ import org.tron.common.zksnark.LibrustzcashParam.IvkToPkdParams; import org.tron.common.zksnark.LibrustzcashParam.OutputProofParams; import org.tron.common.zksnark.LibrustzcashParam.SpendSigParams; +import org.tron.consensus.dpos.DposSlot; +import org.tron.consensus.dpos.DposTask; import org.tron.core.Wallet; import org.tron.core.actuator.Actuator; import org.tron.core.actuator.ActuatorCreator; @@ -140,14 +143,16 @@ public class ShieldedReceiveTest extends BaseTest { @Resource private ConsensusService consensusService; @Resource + private DposTask dposTask; + @Resource private Wallet wallet; @Resource - private TransactionUtil transactionUtil; + private DposSlot dposSlot; private static boolean init; static { - Args.setParam(new String[]{"--output-directory", dbPath(), "-w"}, SHIELD_CONF); + Args.setParam(new String[] {"--output-directory", dbPath(), "-w"}, SHIELD_CONF); ADDRESS_ONE_PRIVATE_KEY = getRandomPrivateKey(); FROM_ADDRESS = getHexAddressByPrivateKey(ADDRESS_ONE_PRIVATE_KEY); } @@ -331,7 +336,7 @@ public void testBroadcastBeforeAllowZksnark() //Add public address sign transactionCap = TransactionUtils.addTransactionSign(transactionCap.getInstance(), - ADDRESS_ONE_PRIVATE_KEY, chainBaseManager.getAccountStore()); + ADDRESS_ONE_PRIVATE_KEY, chainBaseManager.getAccountStore()); try { dbManager.pushTransaction(transactionCap); } catch (Exception e) { @@ -433,7 +438,7 @@ public String[] generateSpendAndOutputParams() throws ZksnarkException, BadItemE boolean ok2 = JLibrustzcash.librustzcashSaplingCheckOutput(checkOutputParams); Assert.assertTrue(ok2); - return new String[]{ByteArray.toHexString(checkSpendParamsData), + return new String[] {ByteArray.toHexString(checkSpendParamsData), ByteArray.toHexString(dataToBeSigned), ByteArray.toHexString(checkOutputParams.encode())}; } @@ -2402,128 +2407,158 @@ public void pushSameSkAndScanAndSpend() throws Exception { assert ecKey != null; byte[] witnessAddress = ecKey.getAddress(); WitnessCapsule witnessCapsule = new WitnessCapsule(ByteString.copyFrom(witnessAddress)); - chainBaseManager.addWitness(ByteString.copyFrom(witnessAddress)); - - //sometimes generate block failed, try several times. - long time = System.currentTimeMillis(); - Block block = getSignedBlock(witnessCapsule.getAddress(), time, privateKey); - dbManager.pushBlock(new BlockCapsule(block)); - - //create transactions - chainBaseManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); - chainBaseManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(1000 * 1000000L); - ZenTransactionBuilder builder = new ZenTransactionBuilder(wallet); - - // generate spend proof - SpendingKey sk = SpendingKey - .decode("ff2c06269315333a9207f817d2eca0ac555ca8f90196976324c7756504e7c9ee"); - ExpandedSpendingKey expsk = sk.expandedSpendingKey(); - byte[] senderOvk = expsk.getOvk(); - PaymentAddress address = sk.defaultAddress(); - Note note = new Note(address, 1000 * 1000000L); - IncrementalMerkleVoucherContainer voucher = createSimpleMerkleVoucherContainer(note.cm()); - byte[] anchor = voucher.root().getContent().toByteArray(); - chainBaseManager.getMerkleContainer() - .putMerkleTreeIntoStore(anchor, voucher.getVoucherCapsule().getTree()); - builder.addSpend(expsk, note, anchor, voucher); - - // generate output proof - SpendingKey sk2 = SpendingKey.random(); - FullViewingKey fullViewingKey = sk2.fullViewingKey(); - IncomingViewingKey incomingViewingKey = fullViewingKey.inViewingKey(); - - byte[] memo = org.tron.keystore.Wallet.generateRandomBytes(512); - - //send coin to 2 different address generated by same sk - DiversifierT d1 = DiversifierT.random(); - PaymentAddress paymentAddress1 = incomingViewingKey.address(d1).get(); - builder.addOutput(senderOvk, paymentAddress1, - (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2, memo); - - DiversifierT d2 = DiversifierT.random(); - PaymentAddress paymentAddress2 = incomingViewingKey.address(d2).get(); - builder.addOutput(senderOvk, paymentAddress2, - (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2, memo); - - TransactionCapsule transactionCap = builder.build(); + // Stop the consensus task before modifying the witness schedule: DposTask uses the same + // localwitness key and would otherwise race to produce blocks at the same slot, + // triggering fork resolution and making the test slow. + consensusService.stop(); + try { + chainBaseManager.addWitness(ByteString.copyFrom(witnessAddress)); + + long time = nextScheduledTime(witnessCapsule.getAddress()); + Block block = getSignedBlock(witnessCapsule.getAddress(), time, privateKey); + dbManager.pushBlock(new BlockCapsule(block)); + + //create transactions + chainBaseManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); + chainBaseManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(1000 * 1000000L); + ZenTransactionBuilder builder = new ZenTransactionBuilder(wallet); + + // generate spend proof + SpendingKey sk = SpendingKey + .decode("ff2c06269315333a9207f817d2eca0ac555ca8f90196976324c7756504e7c9ee"); + ExpandedSpendingKey expsk = sk.expandedSpendingKey(); + byte[] senderOvk = expsk.getOvk(); + PaymentAddress address = sk.defaultAddress(); + Note note = new Note(address, 1000 * 1000000L); + IncrementalMerkleVoucherContainer voucher = createSimpleMerkleVoucherContainer(note.cm()); + byte[] anchor = voucher.root().getContent().toByteArray(); + chainBaseManager.getMerkleContainer() + .putMerkleTreeIntoStore(anchor, voucher.getVoucherCapsule().getTree()); + builder.addSpend(expsk, note, anchor, voucher); + + // generate output proof + SpendingKey sk2 = SpendingKey.random(); + FullViewingKey fullViewingKey = sk2.fullViewingKey(); + IncomingViewingKey incomingViewingKey = fullViewingKey.inViewingKey(); + + byte[] memo = org.tron.keystore.Wallet.generateRandomBytes(512); + + //send coin to 2 different address generated by same sk + DiversifierT d1 = DiversifierT.random(); + PaymentAddress paymentAddress1 = incomingViewingKey.address(d1).get(); + builder.addOutput(senderOvk, paymentAddress1, + (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2, memo); + + DiversifierT d2 = DiversifierT.random(); + PaymentAddress paymentAddress2 = incomingViewingKey.address(d2).get(); + builder.addOutput(senderOvk, paymentAddress2, + (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2, memo); - byte[] trxId = transactionCap.getTransactionId().getBytes(); - boolean ok = dbManager.pushTransaction(transactionCap); - Assert.assertTrue(ok); + TransactionCapsule transactionCap = builder.build(); - Thread.sleep(500); - //package transaction to block - block = getSignedBlock(witnessCapsule.getAddress(), time + 3000, privateKey); - dbManager.pushBlock(new BlockCapsule(block)); - - BlockCapsule blockCapsule3 = new BlockCapsule(wallet.getNowBlock()); - Assert.assertEquals("blocknum != 2", 2, blockCapsule3.getNum()); - - block = getSignedBlock(witnessCapsule.getAddress(), time + 6000, privateKey); - dbManager.pushBlock(new BlockCapsule(block)); - - // scan note by ivk - byte[] receiverIvk = incomingViewingKey.getValue(); - DecryptNotes notes1 = wallet.scanNoteByIvk(0, 100, receiverIvk); - Assert.assertEquals(2, notes1.getNoteTxsCount()); - - // scan note by ivk and mark - DecryptNotesMarked notes3 = wallet.scanAndMarkNoteByIvk(0, 100, receiverIvk, - fullViewingKey.getAk(), fullViewingKey.getNk()); - Assert.assertEquals(2, notes3.getNoteTxsCount()); - - // scan note by ovk - DecryptNotes notes2 = wallet.scanNoteByOvk(0, 100, senderOvk); - Assert.assertEquals(2, notes2.getNoteTxsCount()); - - // to spend received note above. - ZenTransactionBuilder builder2 = new ZenTransactionBuilder(wallet); - - //query merkleinfo - OutputPointInfo.Builder request = OutputPointInfo.newBuilder(); - for (int i = 0; i < notes1.getNoteTxsCount(); i++) { - OutputPoint.Builder outPointBuild = OutputPoint.newBuilder(); - outPointBuild.setHash(ByteString.copyFrom(trxId)); - outPointBuild.setIndex(i); - request.addOutPoints(outPointBuild.build()); - } - request.setBlockNum(1); - IncrementalMerkleVoucherInfo merkleVoucherInfo = wallet - .getMerkleTreeVoucherInfo(request.build()); - - //build spend proof. allow only one note in spend - ExpandedSpendingKey expsk2 = sk2.expandedSpendingKey(); - for (int i = 0; i < 1; i++) { - org.tron.api.GrpcAPI.Note grpcNote = notes1.getNoteTxs(i).getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(grpcNote.getPaymentAddress()); - Note note2 = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), - grpcNote.getValue(), - grpcNote.getRcm().toByteArray() - ); + byte[] trxId = transactionCap.getTransactionId().getBytes(); + boolean ok = dbManager.pushTransaction(transactionCap); + Assert.assertTrue(ok); + + Thread.sleep(500); + //package transaction to block + long expectedBlockNum = chainBaseManager.getDynamicPropertiesStore() + .getLatestBlockHeaderNumber() + 1; + block = getSignedBlock(witnessCapsule.getAddress(), + nextScheduledTime(witnessCapsule.getAddress()), privateKey); + dbManager.pushBlock(new BlockCapsule(block)); + + BlockCapsule blockCapsule3 = new BlockCapsule(wallet.getNowBlock()); + Assert.assertEquals("unexpected block number", expectedBlockNum, blockCapsule3.getNum()); + + block = getSignedBlock(witnessCapsule.getAddress(), + nextScheduledTime(witnessCapsule.getAddress()), privateKey); + dbManager.pushBlock(new BlockCapsule(block)); + + // scan note by ivk + byte[] receiverIvk = incomingViewingKey.getValue(); + DecryptNotes notes1 = wallet.scanNoteByIvk(0, 100, receiverIvk); + Assert.assertEquals(2, notes1.getNoteTxsCount()); + + // scan note by ivk and mark + DecryptNotesMarked notes3 = wallet.scanAndMarkNoteByIvk(0, 100, receiverIvk, + fullViewingKey.getAk(), fullViewingKey.getNk()); + Assert.assertEquals(2, notes3.getNoteTxsCount()); + + // scan note by ovk + DecryptNotes notes2 = wallet.scanNoteByOvk(0, 100, senderOvk); + Assert.assertEquals(2, notes2.getNoteTxsCount()); + + // to spend received note above. + ZenTransactionBuilder builder2 = new ZenTransactionBuilder(wallet); + + //query merkleinfo + OutputPointInfo.Builder request = OutputPointInfo.newBuilder(); + for (int i = 0; i < notes1.getNoteTxsCount(); i++) { + OutputPoint.Builder outPointBuild = OutputPoint.newBuilder(); + outPointBuild.setHash(ByteString.copyFrom(trxId)); + outPointBuild.setIndex(i); + request.addOutPoints(outPointBuild.build()); + } + request.setBlockNum(1); + IncrementalMerkleVoucherInfo merkleVoucherInfo = wallet + .getMerkleTreeVoucherInfo(request.build()); + + //build spend proof. allow only one note in spend + ExpandedSpendingKey expsk2 = sk2.expandedSpendingKey(); + for (int i = 0; i < 1; i++) { + org.tron.api.GrpcAPI.Note grpcNote = notes1.getNoteTxs(i).getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(grpcNote.getPaymentAddress()); + Note note2 = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), + grpcNote.getValue(), + grpcNote.getRcm().toByteArray() + ); + + IncrementalMerkleVoucherContainer voucher2 = + new IncrementalMerkleVoucherContainer( + new IncrementalMerkleVoucherCapsule(merkleVoucherInfo.getVouchers(i))); + byte[] anchor2 = voucher2.root().getContent().toByteArray(); + builder2.addSpend(expsk2, note2, anchor2, voucher2); + } - IncrementalMerkleVoucherContainer voucher2 = - new IncrementalMerkleVoucherContainer( - new IncrementalMerkleVoucherCapsule(merkleVoucherInfo.getVouchers(i))); - byte[] anchor2 = voucher2.root().getContent().toByteArray(); - builder2.addSpend(expsk2, note2, anchor2, voucher2); + //build output proof + SpendingKey sk3 = SpendingKey.random(); + FullViewingKey fvk3 = sk3.fullViewingKey(); + IncomingViewingKey ivk3 = fvk3.inViewingKey(); + + DiversifierT d3 = DiversifierT.random(); + PaymentAddress paymentAddress3 = incomingViewingKey.address(d3).get(); + byte[] memo3 = org.tron.keystore.Wallet.generateRandomBytes(512); + builder2.addOutput(expsk2.getOvk(), paymentAddress3, + (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2 - wallet + .getShieldedTransactionFee(), memo3); + + TransactionCapsule transactionCap2 = builder2.build(); + boolean ok2 = dbManager.pushTransaction(transactionCap2); + Assert.assertTrue(ok2); + } finally { + // DposTask.init() does not reset isRunning (it stays false after stop()), so force it back + // to true via reflection before restarting. + Field isRunning = DposTask.class.getDeclaredField("isRunning"); + isRunning.setAccessible(true); + isRunning.set(dposTask, true); + consensusService.start(); + } + } + + // Returns the earliest timestamp at which witnessAddr is the DPoS-scheduled producer, + // relative to the current chain head. Using this avoids relying on the genesis-only + // bypass in validBlock() (latestBlockHeaderNumber == 0) when prior tests have pushed blocks. + private long nextScheduledTime(ByteString witnessAddr) { + int size = chainBaseManager.getWitnessScheduleStore().getActiveWitnesses().size(); + for (long slot = 1; slot <= size; slot++) { + if (dposSlot.getScheduledWitness(slot).equals(witnessAddr)) { + return dposSlot.getTime(slot); + } } - - //build output proof - SpendingKey sk3 = SpendingKey.random(); - FullViewingKey fvk3 = sk3.fullViewingKey(); - IncomingViewingKey ivk3 = fvk3.inViewingKey(); - - DiversifierT d3 = DiversifierT.random(); - PaymentAddress paymentAddress3 = incomingViewingKey.address(d3).get(); - byte[] memo3 = org.tron.keystore.Wallet.generateRandomBytes(512); - builder2.addOutput(expsk2.getOvk(), paymentAddress3, - (1000 * 1000000L - wallet.getShieldedTransactionFee()) / 2 - wallet - .getShieldedTransactionFee(), memo3); - - TransactionCapsule transactionCap2 = builder2.build(); - boolean ok2 = dbManager.pushTransaction(transactionCap2); - Assert.assertTrue(ok2); + throw new IllegalStateException("No scheduled slot for witness within " + + size + " slots: " + ByteArray.toHexString(witnessAddr.toByteArray())); } @Test diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java index 7842eed848..ade00374bc 100755 --- a/framework/src/test/java/org/tron/program/SolidityNodeTest.java +++ b/framework/src/test/java/org/tron/program/SolidityNodeTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -329,6 +330,47 @@ public void testGetBlockByNumWhenClosed() throws Exception { } } + /** + * getBlockByNum() must break immediately — without a 1-second sleep — when a + * gRPC exception is thrown while flag races to false (the P3 shutdown-race fix). + * The invocation time is measured directly so the assertion is independent of + * Spring-context startup overhead. + */ + @Test(timeout = 5000) + public void testGetBlockByNumNoErrorOnExceptionDuringShutdown() throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + Field clientField = getField("databaseGrpcClient"); + Object origClient = clientField.get(solidityNode); + setFlag(true); // precondition: while(flag) must be entered; do not rely on test-ordering + try { + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + // flag races to false inside the gRPC call — exact close() race + Mockito.when(mockClient.getBlock(42L)).thenAnswer(inv -> { + setFlag(false); + throw new RuntimeException("channel closed during shutdown"); + }); + clientField.set(solidityNode, mockClient); + + long start = System.currentTimeMillis(); + InvocationTargetException t = assertThrows(InvocationTargetException.class, () -> { + m.invoke(solidityNode, 42L); + }); + assertTrue(t.getCause() instanceof RuntimeException); + assertEquals("SolidityNode is closing.", t.getCause().getMessage()); + long elapsed = System.currentTimeMillis() - start; + // Without the fix the catch sleeps exceptionSleepTime (1000 ms) before + // re-checking the while condition. With the fix it breaks immediately. + assertTrue("Expected break without sleep (<500 ms), got " + elapsed + " ms", + elapsed < 500); + // No retry: exactly one gRPC call must be made. + Mockito.verify(mockClient, Mockito.times(1)).getBlock(42L); + } finally { + setFlag(true); + clientField.set(solidityNode, origClient); + } + } + // ── getLastSolidityBlockNum() ───────────────────────────────────────────────── /** From 7cda02d098b5bef48a86d6a2425f3a81d1a351d5 Mon Sep 17 00:00:00 2001 From: Jeremy Zhang <50477615+warku123@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:04:08 +0800 Subject: [PATCH 04/13] feat(ci): add integration test workflows (single-node + multinode) (#6789) --- .../workflows/integration-test-multinode.yml | 119 ++++++++++++++++++ .../integration-test-single-node.yml | 80 ++++++++++++ .github/workflows/pr-cancel.yml | 66 ++++++---- .github/workflows/system-test.yml | 95 -------------- 4 files changed, 238 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/integration-test-multinode.yml create mode 100644 .github/workflows/integration-test-single-node.yml delete mode 100644 .github/workflows/system-test.yml diff --git a/.github/workflows/integration-test-multinode.yml b/.github/workflows/integration-test-multinode.yml new file mode 100644 index 0000000000..99a7b54e02 --- /dev/null +++ b/.github/workflows/integration-test-multinode.yml @@ -0,0 +1,119 @@ +name: Integration Test Multinode (Full) + +on: + push: + branches: [ 'master', 'release_**' ] + pull_request: + branches: [ 'develop', 'release_**' ] + types: [ opened, synchronize, reopened ] + paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + multinode-full: + name: Integration Test Multinode Full (JDK 8 / x86_64) + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout java-tron + uses: actions/checkout@v5 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-multinode-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle-multinode- + + - name: Build FullNode.jar + run: ./gradlew clean build -x test --no-daemon + + - name: Build local java-tron Docker image (wraps PR-built FullNode.jar) + run: | + mkdir -p /tmp/tron-image + cp build/libs/FullNode.jar /tmp/tron-image/ + cat > /tmp/tron-image/Dockerfile <<'EOF' + FROM tronprotocol/java-tron:latest + COPY FullNode.jar /java-tron/lib/FullNode.jar + EOF + docker build -t java-tron-local:pr /tmp/tron-image + + - name: Pull integration-test image + run: docker pull troninfra/troninfra-ci:latest + + - name: Extract compose configs to host (for DinD path-alignment) + run: | + # start-multinode.sh builds HOST_COMPOSE_DIR as: + # ${HOST_WORKDIR}/docker/multi-node + # so the files must live at $HOST_WORKDIR/docker/multi-node/ on the + # host. Set HOST_WORKDIR to the workspace root and extract + # /app/docker/ 1:1 into workspace/docker/ — the subdirectories + # (multi-node/, single-node/) don't collide with java-tron's own + # docker/ files. + docker create --name it-extract troninfra/troninfra-ci:latest + docker cp it-extract:/app/docker/. "${{ github.workspace }}/docker/" + docker rm -f it-extract + + - name: Run multinode full tests + run: | + # --network host: multinode tests talk to nodes via 127.0.0.1:50051 etc. + # DinD socket + HOST_WORKDIR path-alignment lets the container orchestrate + # the 3-witness compose stack via the host daemon. + # Don't override --workdir so the container's default /app entrypoint works. + docker run --name integration-multinode \ + --network host \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${{ github.workspace }}:${{ github.workspace }}" \ + -v "${{ github.workspace }}/docker/multi-node:/app/docker/multi-node" \ + -e HOST_WORKDIR="${{ github.workspace }}" \ + -e TRON_IMAGE=java-tron-local:pr \ + -e JAVA_HOME=/usr/lib/jvm/temurin-8 \ + -e JAVA_HOME_17=/opt/java/openjdk \ + troninfra/troninfra-ci:latest \ + --multinode --clean + + - name: Extract test reports from container + if: always() + run: | + mkdir -p integration-reports + docker cp integration-multinode:/app/build/reports/. integration-reports/reports/ 2>/dev/null || true + docker cp integration-multinode:/app/build/test-results/. integration-reports/test-results/ 2>/dev/null || true + docker cp integration-multinode:/app/build/test-output.log integration-reports/ 2>/dev/null || true + + - name: Collect witness node logs + if: always() + run: | + mkdir -p integration-reports/node-logs + for c in tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb; do + docker logs "$c" > "integration-reports/node-logs/${c}.log" 2>&1 || true + done + + - name: Tear down compose stack + if: always() + run: | + docker rm -f tron-mn-node1 tron-mn-node2 tron-mn-node3 tron-mn-mongodb 2>/dev/null || true + docker network rm multi-node_tron-net 2>/dev/null || true + docker rm -f integration-multinode 2>/dev/null || true + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v6 + with: + name: integration-multinode-report + path: integration-reports/ + if-no-files-found: warn diff --git a/.github/workflows/integration-test-single-node.yml b/.github/workflows/integration-test-single-node.yml new file mode 100644 index 0000000000..a68c243efc --- /dev/null +++ b/.github/workflows/integration-test-single-node.yml @@ -0,0 +1,80 @@ +name: Integration Test Single Node (Full) + +on: + push: + branches: [ 'master', 'release_**' ] + pull_request: + branches: [ 'develop', 'release_**' ] + types: [ opened, synchronize, reopened ] + paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + integration: + name: Integration Test Single Node Full (JDK 8 / x86_64) + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout java-tron + uses: actions/checkout@v5 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-integration-test-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle-integration-test- + + - name: Build FullNode.jar + run: ./gradlew clean build -x test --no-daemon + + - name: Pull integration-test image + run: docker pull troninfra/troninfra-ci:latest + + - name: Run integration tests + run: | + # JAVA_HOME=JDK 8 so FullNode runs on the same JVM family as + # production (a few assertions check `java.version` starts with + # "1.8"). JAVA_HOME_17 keeps Gradle on JDK 17 for the test + # tooling, which requires Java 17. + docker run --name integration-test \ + -e FULLNODE_JAR=/javatron/FullNode.jar \ + -e JAVA_HOME=/usr/lib/jvm/temurin-8 \ + -e JAVA_HOME_17=/opt/java/openjdk \ + -v "${{ github.workspace }}/build/libs/FullNode.jar:/javatron/FullNode.jar:ro" \ + troninfra/troninfra-ci:latest \ + --clean + + - name: Extract test reports from container + if: always() + run: | + mkdir -p integration-reports + docker cp integration-test:/app/build/reports/. integration-reports/reports/ 2>/dev/null || true + docker cp integration-test:/app/build/test-results/. integration-reports/test-results/ 2>/dev/null || true + docker cp integration-test:/app/build/test-output.log integration-reports/ 2>/dev/null || true + docker cp integration-test:/app/node/node.log integration-reports/ 2>/dev/null || true + docker cp integration-test:/app/node/data/logs/tron.log integration-reports/ 2>/dev/null || true + docker rm -f integration-test 2>/dev/null || true + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v6 + with: + name: integration-test-report + path: integration-reports/ + if-no-files-found: warn diff --git a/.github/workflows/pr-cancel.yml b/.github/workflows/pr-cancel.yml index bbd0e68c23..3213026d3f 100644 --- a/.github/workflows/pr-cancel.yml +++ b/.github/workflows/pr-cancel.yml @@ -13,43 +13,55 @@ jobs: if: github.event.pull_request.merged == false runs-on: ubuntu-latest steps: - - name: Cancel PR Build and System Test + - name: Cancel PR Build and Integration Tests uses: actions/github-script@v8 with: script: | - const workflows = ['pr-build.yml', 'system-test.yml', 'codeql.yml']; + const workflows = [ + 'pr-build.yml', + 'codeql.yml', + 'integration-test-single-node.yml', + 'integration-test-multinode.yml', + ]; const headSha = context.payload.pull_request.head.sha; const prNumber = context.payload.pull_request.number; for (const workflowId of workflows) { - for (const status of ['in_progress', 'queued']) { - const runs = await github.paginate( - github.rest.actions.listWorkflowRuns, - { - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: workflowId, - status, - event: 'pull_request', - per_page: 100, - }, - (response) => response.data.workflow_runs - ); - - for (const run of runs) { - if (!run) { - continue; - } - const prs = Array.isArray(run.pull_requests) ? run.pull_requests : []; - const isTargetPr = prs.length === 0 || prs.some((pr) => pr.number === prNumber); - if (run.head_sha === headSha && isTargetPr) { - await github.rest.actions.cancelWorkflowRun({ + // Wrap each workflow iteration so a missing / renamed file + // doesn't take down the whole cancel job — other workflows + // in the list still get processed. + try { + for (const status of ['in_progress', 'queued']) { + const runs = await github.paginate( + github.rest.actions.listWorkflowRuns, + { owner: context.repo.owner, repo: context.repo.repo, - run_id: run.id, - }); - console.log(`Cancelled ${workflowId} run #${run.id} (${status})`); + workflow_id: workflowId, + status, + event: 'pull_request', + per_page: 100, + }, + (response) => response.data.workflow_runs + ); + + for (const run of runs) { + if (!run) { + continue; + } + const prs = Array.isArray(run.pull_requests) ? run.pull_requests : []; + const isTargetPr = prs.length === 0 || prs.some((pr) => pr.number === prNumber); + if (run.head_sha === headSha && isTargetPr) { + await github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + console.log(`Cancelled ${workflowId} run #${run.id} (${status})`); + } } } + } catch (err) { + console.log(`Skipping ${workflowId}: ${err.message}`); } } diff --git a/.github/workflows/system-test.yml b/.github/workflows/system-test.yml deleted file mode 100644 index f6184fb0ef..0000000000 --- a/.github/workflows/system-test.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: System Test - -on: - push: - branches: [ 'master', 'release_**' ] - pull_request: - branches: [ 'develop', 'release_**' ] - types: [ opened, synchronize, reopened ] - paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', - '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', - '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - system-test: - name: System Test (JDK 8 / x86_64) - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Set up JDK 8 - uses: actions/setup-java@v5 - with: - java-version: '8' - distribution: 'temurin' - - - name: Clone system-test - uses: actions/checkout@v5 - with: - repository: tronprotocol/system-test - ref: release_workflow - path: system-test - - - name: Checkout java-tron - uses: actions/checkout@v5 - with: - path: java-tron - - - name: Cache Gradle packages - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-system-test-${{ hashFiles('java-tron/**/*.gradle', 'java-tron/**/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle-system-test- - - - name: Build java-tron - working-directory: java-tron - run: ./gradlew clean build -x test --no-daemon - - - name: Copy config and start FullNode - run: | - cp system-test/testcase/src/test/resources/config-system-test.conf java-tron/ - cd java-tron - nohup java -jar build/libs/FullNode.jar --witness -c config-system-test.conf > fullnode.log 2>&1 & - echo "FullNode started, waiting for it to be ready..." - - MAX_ATTEMPTS=60 - INTERVAL=5 - for i in $(seq 1 $MAX_ATTEMPTS); do - if curl -s --fail "http://localhost:8090/wallet/getblockbynum?num=1" > /dev/null 2>&1; then - echo "FullNode is ready! (attempt $i)" - exit 0 - fi - echo "Waiting... (attempt $i/$MAX_ATTEMPTS)" - sleep $INTERVAL - done - - echo "FullNode failed to start within $((MAX_ATTEMPTS * INTERVAL)) seconds." - echo "=== FullNode log (last 50 lines) ===" - tail -50 fullnode.log || true - exit 1 - - - name: Run system tests - working-directory: system-test - run: | - if [ ! -f solcDIR/solc-linux-0.8.6 ]; then - echo "ERROR: solc binary not found at solcDIR/solc-linux-0.8.6" - exit 1 - fi - cp solcDIR/solc-linux-0.8.6 solcDIR/solc - ./gradlew clean --no-daemon - ./gradlew --info stest --no-daemon - - - name: Upload FullNode log - if: always() - uses: actions/upload-artifact@v6 - with: - name: fullnode-log - path: java-tron/fullnode.log - if-no-files-found: warn From ba5b0127439b14a8f6da77b878286e7ca5527f6e Mon Sep 17 00:00:00 2001 From: xxo1_shine Date: Wed, 3 Jun 2026 16:02:24 +0800 Subject: [PATCH 05/13] fix(merkle): build MerkleTree per-instance to fix concurrent race (#6816) MerkleTree was a shared volatile singleton holding per-build mutable state (leaves/hashList/root). When merkle validation runs concurrently (e.g. pre-broadcast validation on the P2P handler threads alongside the apply path), concurrent calls mutated the shared leaves list and threw ArrayIndexOutOfBoundsException (surfaced on an ARM SR due to its weaker memory model), which escaped the BadBlockException catch and dropped peers. - MerkleTree: replace the getInstance() singleton with a static build() factory returning a thread-confined instance; drop the now-inaccurate @NotThreadSafe. - BlockCapsule.calcMerkleRoot: use MerkleTree.build(ids); keeps the config-driven hash engine, so no consensus behavior change. - MerkleTreeTest: switch call sites to build(); un-ignore testConcurrent and turn it into a regression test asserting 1000 concurrent builds succeed. --- .../org/tron/core/capsule/BlockCapsule.java | 2 +- .../tron/core/capsule/utils/MerkleTree.java | 29 +++++------------ .../core/capsule/utils/MerkleTreeTest.java | 31 ++++++++----------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java index 63acf64b64..e6cbd52e59 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -227,7 +227,7 @@ public Sha256Hash calcMerkleRoot() { .map(TransactionCapsule::getMerkleHash) .collect(Collectors.toCollection(ArrayList::new)); - return MerkleTree.getInstance().createTree(ids).getRoot().getHash(); + return MerkleTree.build(ids).getRoot().getHash(); } public void validateMerkleRoot() throws BadBlockException { diff --git a/chainbase/src/main/java/org/tron/core/capsule/utils/MerkleTree.java b/chainbase/src/main/java/org/tron/core/capsule/utils/MerkleTree.java index 94d22f4b47..cb6f299e87 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/utils/MerkleTree.java +++ b/chainbase/src/main/java/org/tron/core/capsule/utils/MerkleTree.java @@ -5,41 +5,28 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.Getter; -import net.jcip.annotations.NotThreadSafe; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; @Getter -@NotThreadSafe public class MerkleTree { - private static volatile MerkleTree instance; private List hashList; private List leaves; private Leaf root; - public static MerkleTree getInstance() { - if (instance == null) { - synchronized (MerkleTree.class) { - if (instance == null) { - instance = new MerkleTree(); - } - } - } - return instance; - } - - public MerkleTree createTree(List hashList) { - this.leaves = new ArrayList<>(); - this.hashList = hashList; - List leaves = createLeaves(hashList); + public static MerkleTree build(List hashList) { + MerkleTree tree = new MerkleTree(); + tree.hashList = hashList; + tree.leaves = new ArrayList<>(); + List leaves = tree.createLeaves(hashList); while (leaves.size() > 1) { - leaves = createParentLeaves(leaves); + leaves = tree.createParentLeaves(leaves); } - this.root = leaves.get(0); - return this; + tree.root = leaves.get(0); + return tree; } private List createParentLeaves(List leaves) { diff --git a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java index 88e95f9653..c9fea6bce4 100644 --- a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java +++ b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java @@ -10,10 +10,7 @@ import java.util.stream.IntStream; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.MerkleRoot; @@ -23,9 +20,6 @@ @Slf4j public class MerkleTreeTest { - @Rule - public ExpectedException exception = ExpectedException.none(); - private static List getHash(int hashNum) { List hashList = new ArrayList(); for (int i = 0; i < hashNum; i++) { @@ -102,7 +96,7 @@ private static int getRank(int num) { public void test0HashNum() { List hashList = getHash(0); //Empty list. Exception e = Assert.assertThrows(Exception.class, - () -> MerkleTree.getInstance().createTree(hashList)); + () -> MerkleTree.build(hashList)); Assert.assertTrue(e instanceof IndexOutOfBoundsException); } @@ -117,7 +111,7 @@ public void test0HashNum() { */ public void test1HashNum() { List hashList = getHash(1); - MerkleTree tree = MerkleTree.getInstance().createTree(hashList); + MerkleTree tree = MerkleTree.build(hashList); Leaf root = tree.getRoot(); Assert.assertEquals(root.getHash(), hashList.get(0)); @@ -140,7 +134,7 @@ public void test1HashNum() { */ public void test2HashNum() { List hashList = getHash(2); - MerkleTree tree = MerkleTree.getInstance().createTree(hashList); + MerkleTree tree = MerkleTree.build(hashList); Leaf root = tree.getRoot(); Assert.assertEquals(root.getHash(), computeHash(hashList.get(0), hashList.get(1))); @@ -178,14 +172,13 @@ public void testAnyHashNum() { for (int hashNum = 1; hashNum <= maxNum; hashNum++) { int maxRank = getRank(hashNum); List hashList = getHash(hashNum); - MerkleTree tree = MerkleTree.getInstance().createTree(hashList); + MerkleTree tree = MerkleTree.build(hashList); Leaf root = tree.getRoot(); pareTree(root, hashList, maxRank, 0, 0); } } @Test - @Ignore public void testConcurrent() { Sha256Hash root1 = Sha256Hash.wrap( ByteString.fromHex("6cb38b4f493db8bacf26123cd4253bbfc530c708b97b3747e782f64097c3c482")); @@ -197,14 +190,16 @@ public void testConcurrent() { List list2 = IntStream.range(0, 10000).mapToObj(i -> Sha256Hash.of(true, ("byte2-" + i).getBytes(StandardCharsets.UTF_8))) .collect(Collectors.toList()); - Assert.assertEquals(root1, MerkleTree.getInstance().createTree(list1).getRoot().getHash()); - Assert.assertEquals(root2, MerkleTree.getInstance().createTree(list2).getRoot().getHash()); + Assert.assertEquals(root1, MerkleTree.build(list1).getRoot().getHash()); + Assert.assertEquals(root2, MerkleTree.build(list2).getRoot().getHash()); Assert.assertEquals(root1, MerkleRoot.root(list1)); Assert.assertEquals(root2, MerkleRoot.root(list2)); - exception.expect(ArrayIndexOutOfBoundsException.class); - IntStream.range(0, 1000).parallel().forEach(i -> Assert.assertEquals( - MerkleTree.getInstance().createTree(i % 2 == 0 ? list1 : list2).getRoot().getHash(), - MerkleRoot.root(i % 2 == 0 ? list1 : list2)) - ); + // MerkleTree.build is now per-instance with no shared state, so concurrent builds + // must yield correct roots without ArrayIndexOutOfBoundsException. + IntStream.range(0, 1000).parallel().forEach(i -> { + List list = i % 2 == 0 ? list1 : list2; + Sha256Hash expect = i % 2 == 0 ? root1 : root2; + Assert.assertEquals(expect, MerkleTree.build(list).getRoot().getHash()); + }); } } From 4e80f8ffa9a24e2fe66f48131200954fd3d96c60 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:14:40 +0800 Subject: [PATCH 06/13] feat(version): merge master to 4.8.2 (#6817) * feat(*): disable exchange transaction (#6507) * update a new version. version name:GreatVoyage-v4.8.0-1-g45e3bf88ca,version code:18634 (#6508) * Merge release_v4.8.1 to master (#6541) * update a new version. version name:GreatVoyage-v4.8.0.1-1-g44a4bc8263,version code:18636 (#6542) * feat(vm): optimize the check for create2 * feat(vm): optimize the check for ModExp * test(vm): add tests for create2/modExp checks * feat(version): update version to 4.8.1.1 * feat(ci): add PR pipeline and system-test workflows New workflows: - pr-build.yml: multi-OS build matrix (macOS, Ubuntu, RockyLinux, Debian11) and changed-line/overall coverage gate - pr-check.yml: PR title/body lint + Checkstyle - pr-reviewer.yml: scope-based reviewer auto-assignment - pr-cancel.yml: cancel in-progress runs when PR is closed unmerged - system-test.yml: spin up FullNode and run the system-test suite Existing workflows: - codeql.yml: bump to v4/v5 actions, switch to manual build-mode with JDK 8, add paths-ignore for docs-only changes - math-check.yml: bump checkout/upload-artifact/github-script versions * feat(config): fix git.properties NPE * update a new version. version name:GreatVoyage-v4.8.1-6-g52d7d9d23e,version code:18643 --------- Co-authored-by: YAaron <4241080+kuny0707@users.noreply.github.com> Co-authored-by: zz --- .../tron/core/vm/PrecompiledContracts.java | 4 + .../org/tron/core/vm/program/Program.java | 3 + .../java/org/tron/core/vm/utils/MUtil.java | 12 ++ .../java/org/tron/core/config/Parameter.java | 5 +- .../main/java/org/tron/program/Version.java | 4 +- .../runtime/vm/Create2ModExpForkTest.java | 178 ++++++++++++++++++ 6 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/Create2ModExpForkTest.java diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 0dc8fb31ad..3993e8ed83 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -699,6 +699,10 @@ public Pair execute(byte[] data) { return Pair.of(false, EMPTY_BYTE_ARRAY); } + if (baseLen == 0 && modLen == 0 && expLen > UPPER_BOUND) { + MUtil.checkCPUTimeForModExp(); + } + BigInteger base = parseArg(data, ARGS_OFFSET, baseLen); BigInteger exp = parseArg(data, addSafely(ARGS_OFFSET, baseLen), expLen); BigInteger mod = parseArg(data, addSafely(addSafely(ARGS_OFFSET, baseLen), expLen), modLen); diff --git a/actuator/src/main/java/org/tron/core/vm/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 3ed968e1af..41822df239 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Program.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Program.java @@ -1625,6 +1625,9 @@ public void createContract2(DataWord value, DataWord memStart, DataWord memSize, stackPushZero(); return; } + if (getCallDeep() == MAX_DEPTH) { + MUtil.checkCPUTimeForCreate2(); + } if (VMConfig.allowTvmIstanbul()) { senderAddress = getContextAddress(); } else { diff --git a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java index c94f28b3a2..e07360e686 100644 --- a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java +++ b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java @@ -64,4 +64,16 @@ public static void checkCPUTime() { throw new OutOfTimeException("CPU timeout for 0x0a executing"); } } + + public static void checkCPUTimeForCreate2() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_1_1)) { + throw new OutOfTimeException("CPU timeout for create2 executing"); + } + } + + public static void checkCPUTimeForModExp() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_1_1)) { + throw new OutOfTimeException("CPU timeout for modExp executing"); + } + } } diff --git a/common/src/main/java/org/tron/core/config/Parameter.java b/common/src/main/java/org/tron/core/config/Parameter.java index 5349ef8d87..233f1d9ef7 100644 --- a/common/src/main/java/org/tron/core/config/Parameter.java +++ b/common/src/main/java/org/tron/core/config/Parameter.java @@ -29,7 +29,8 @@ public enum ForkBlockVersionEnum { VERSION_4_8_0(32, 1596780000000L, 80), VERSION_4_8_0_1(33, 1596780000000L, 70), VERSION_4_8_1(34, 1596780000000L, 80), - VERSION_4_8_2(35, 1596780000000L, 80); + VERSION_4_8_1_1(35, 1596780000000L, 70), + VERSION_4_8_2(36, 1596780000000L, 80); // if add a version, modify BLOCK_VERSION simultaneously @Getter @@ -78,7 +79,7 @@ public class ChainConstant { public static final int SINGLE_REPEAT = 1; public static final int BLOCK_FILLED_SLOTS_NUMBER = 128; public static final int MAX_FROZEN_NUMBER = 1; - public static final int BLOCK_VERSION = 35; + public static final int BLOCK_VERSION = 36; public static final long FROZEN_PERIOD = 86_400_000L; public static final long DELEGATE_PERIOD = 3 * 86_400_000L; public static final long TRX_PRECISION = 1000_000L; diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index 3ce7ce2031..73e4f1e826 100644 --- a/framework/src/main/java/org/tron/program/Version.java +++ b/framework/src/main/java/org/tron/program/Version.java @@ -2,8 +2,8 @@ public class Version { - public static final String VERSION_NAME = "GreatVoyage-v4.8.0.1-1-g44a4bc8263"; - public static final String VERSION_CODE = "18636"; + public static final String VERSION_NAME = "GreatVoyage-v4.8.1-6-g52d7d9d23e"; + public static final String VERSION_CODE = "18643"; private static final String VERSION = "4.8.2"; public static String getVersion() { diff --git a/framework/src/test/java/org/tron/common/runtime/vm/Create2ModExpForkTest.java b/framework/src/test/java/org/tron/common/runtime/vm/Create2ModExpForkTest.java new file mode 100644 index 0000000000..6fbecb4c87 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/Create2ModExpForkTest.java @@ -0,0 +1,178 @@ +package org.tron.common.runtime.vm; + +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.runtime.InternalTransaction; +import org.tron.common.utils.ForkController; +import org.tron.core.Constant; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.core.vm.program.invoke.ProgramInvokeMockImpl; +import org.tron.core.vm.utils.MUtil; +import org.tron.protos.Protocol; + + +@Slf4j +public class Create2ModExpForkTest extends BaseTest { + + // mirrors the private Program.MAX_DEPTH + private static final int MAX_CALL_DEPTH = 64; + + // mirrors PrecompiledContracts.ModExp.UPPER_BOUND + private static final int MOD_EXP_UPPER_BOUND = 1024; + + // ModExp precompile address (0x05) + private static final DataWord MOD_EXP_ADDR = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000005"); + + @BeforeClass + public static void init() { + Args.setParam(new String[]{"--output-directory", dbPath(), "--debug"}, TestConstants.TEST_CONF); + CommonParameter.getInstance().setDebug(true); + } + + @AfterClass + public static void destroy() { + ConfigLoader.disable = false; + VMConfig.initVmHardFork(false); + VMConfig.initAllowTvmCompatibleEvm(0); + Args.clearParam(); + } + + @Before + public void setUp() { + ForkController.instance().init(chainBaseManager); + deactivateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + } + + @Test + public void checkCPUTimeForCreate2_isGatedByFork() { + MUtil.checkCPUTimeForCreate2(); + + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + OutOfTimeException ex = + Assert.assertThrows(OutOfTimeException.class, MUtil::checkCPUTimeForCreate2); + Assert.assertEquals("CPU timeout for create2 executing", ex.getMessage()); + } + + @Test + public void checkCPUTimeForModExp_isGatedByFork() { + MUtil.checkCPUTimeForModExp(); + + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + OutOfTimeException ex = + Assert.assertThrows(OutOfTimeException.class, MUtil::checkCPUTimeForModExp); + Assert.assertEquals("CPU timeout for modExp executing", ex.getMessage()); + } + + @Test + public void modExp_degenerateInput_throwsOnlyAfterFork() { + PrecompiledContract modExp = PrecompiledContracts.getContractForAddress(MOD_EXP_ADDR); + byte[] data = buildModExpInput(MOD_EXP_UPPER_BOUND + 1); + + Pair out = modExp.execute(data); + Assert.assertTrue(out.getLeft()); + + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + OutOfTimeException ex = + Assert.assertThrows(OutOfTimeException.class, () -> modExp.execute(data)); + Assert.assertEquals("CPU timeout for modExp executing", ex.getMessage()); + } + + @Test + public void modExp_atUpperBound_doesNotThrowAfterFork() { + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + PrecompiledContract modExp = PrecompiledContracts.getContractForAddress(MOD_EXP_ADDR); + Pair out = modExp.execute(buildModExpInput(MOD_EXP_UPPER_BOUND)); + Assert.assertTrue(out.getLeft()); + } + + @Test + public void createContract2_atMaxDepth_legacyPath_throwsAfterFork() + throws ContractValidateException { + VMConfig.initAllowTvmCompatibleEvm(0); + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + Program program = buildProgramAtMaxDepth(); + OutOfTimeException ex = Assert.assertThrows(OutOfTimeException.class, + () -> program.createContract2( + DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO())); + Assert.assertEquals("CPU timeout for create2 executing", ex.getMessage()); + } + + @Test + public void createContract2_atMaxDepth_compatibleEvmOn_doesNotThrow() + throws ContractValidateException { + VMConfig.initAllowTvmCompatibleEvm(1); + activateFork(ForkBlockVersionEnum.VERSION_4_8_1_1); + + Program program = buildProgramAtMaxDepth(); + program.createContract2(DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO()); + Assert.assertEquals(DataWord.ZERO(), program.getStack().pop()); + } + + // ---- helpers --------------------------------------------------------------------------------- + + private Program buildProgramAtMaxDepth() throws ContractValidateException { + StoreFactory.init(); + StoreFactory storeFactory = StoreFactory.getInstance(); + storeFactory.setChainBaseManager(chainBaseManager); + byte[] ops = new byte[] {0}; + ProgramInvokeMockImpl invoke = new ProgramInvokeMockImpl(storeFactory, ops, ops) { + @Override + public int getCallDeep() { + return MAX_CALL_DEPTH; + } + }; + Program program = new Program(ops, ops, invoke, + new InternalTransaction(Protocol.Transaction.getDefaultInstance(), + InternalTransaction.TrxType.TRX_UNKNOWN_TYPE)); + program.setRootTransactionId(new byte[32]); + return program; + } + + private byte[] buildModExpInput(int expLen) { + byte[] data = new byte[96]; + byte[] expLenWord = new DataWord(expLen).getData(); + System.arraycopy(expLenWord, 0, data, 32, 32); + return data; + } + + private void activateFork(ForkBlockVersionEnum forkVersion) { + byte[] stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + chainBaseManager.getDynamicPropertiesStore().statsByVersion(forkVersion.getValue(), stats); + long maintenanceTimeInterval = + chainBaseManager.getDynamicPropertiesStore().getMaintenanceTimeInterval(); + long hardForkTime = ((forkVersion.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(hardForkTime + 1); + } + + private void deactivateFork(ForkBlockVersionEnum forkVersion) { + chainBaseManager.getDynamicPropertiesStore() + .statsByVersion(forkVersion.getValue(), new byte[27]); + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(0L); + } +} From c9f604f16e298c1013400ec8dd7f8a7a5cce2206 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 5 Jun 2026 11:11:52 +0800 Subject: [PATCH 07/13] fix(ci): upgrade actions/cache to v5 and fix container git checkout (#6808) --- .../workflows/integration-test-multinode.yml | 2 +- .../integration-test-single-node.yml | 2 +- .github/workflows/pr-build.yml | 32 +++++++++---------- .github/workflows/pr-check.yml | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/integration-test-multinode.yml b/.github/workflows/integration-test-multinode.yml index 99a7b54e02..fadfc2168d 100644 --- a/.github/workflows/integration-test-multinode.yml +++ b/.github/workflows/integration-test-multinode.yml @@ -32,7 +32,7 @@ jobs: distribution: 'temurin' - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches diff --git a/.github/workflows/integration-test-single-node.yml b/.github/workflows/integration-test-single-node.yml index a68c243efc..b0c10247a7 100644 --- a/.github/workflows/integration-test-single-node.yml +++ b/.github/workflows/integration-test-single-node.yml @@ -32,7 +32,7 @@ jobs: distribution: 'temurin' - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index dd005f98b7..191d8be778 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -43,7 +43,7 @@ jobs: distribution: 'temurin' - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -82,7 +82,7 @@ jobs: run: java -version - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -117,20 +117,20 @@ jobs: LC_ALL: en_US.UTF-8 steps: - - name: Checkout code - uses: actions/checkout@v5 - - name: Install dependencies (Rocky 8 + JDK8) run: | set -euxo pipefail dnf -y install java-1.8.0-openjdk-devel git wget unzip which jq bc curl glibc-langpack-en dnf -y groupinstall "Development Tools" + - name: Checkout code + uses: actions/checkout@v5 + - name: Check Java version run: java -version - name: Cache Gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | /github/home/.gradle/caches @@ -174,20 +174,20 @@ jobs: GRADLE_USER_HOME: /github/home/.gradle steps: - - name: Checkout code - uses: actions/checkout@v5 - - name: Install dependencies (Debian + build tools) run: | set -euxo pipefail apt-get update apt-get install -y git wget unzip build-essential curl jq + - name: Checkout code + uses: actions/checkout@v5 + - name: Check Java version run: java -version - name: Cache Gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | /github/home/.gradle/caches @@ -238,19 +238,19 @@ jobs: contents: read steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.base.sha }} - - name: Install dependencies (Debian + build tools) run: | set -euxo pipefail apt-get update apt-get install -y git wget unzip build-essential curl jq + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | /github/home/.gradle/caches diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7ae169a869..b6988c2c4d 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -126,7 +126,7 @@ jobs: distribution: 'temurin' - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches From a9420c03e013add14a141da30f4205218a3845e6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 11:13:48 +0800 Subject: [PATCH 08/13] test(config): add reference.conf to bean parity gate (#6803) --- .../core/config/args/ConfigParityCheck.java | 713 ++++++++++++++++++ .../config/args/ConfigParityGateTest.java | 238 ++++++ 2 files changed, 951 insertions(+) create mode 100644 common/src/test/java/org/tron/core/config/args/ConfigParityCheck.java create mode 100644 common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java diff --git a/common/src/test/java/org/tron/core/config/args/ConfigParityCheck.java b/common/src/test/java/org/tron/core/config/args/ConfigParityCheck.java new file mode 100644 index 0000000000..051ebeaef0 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/ConfigParityCheck.java @@ -0,0 +1,713 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueType; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import lombok.extern.slf4j.Slf4j; + +/** + * Shared helpers for reference.conf <-> {@code *Config} bean parity tests. + * Asserts that every HOCON key under a section binds to a writable bean + * property and matches the bean's default. Drift fails the build at PR time + * instead of waiting for {@code ConfigBeanFactory} to throw at startup. + *

+ * {@code [parity-*]} audit log lines land in the JUnit XML {@code } + * when the gate runs in isolation; if mixed with tests that boot a tron main, + * production logback may redirect them to {@code logs/} on disk. + */ +@Slf4j(topic = "test") +final class ConfigParityCheck { + + private ConfigParityCheck() { + + } + + private static Map writablePropertyDescriptors(Class beanClass) { + try { + Map m = new TreeMap<>(); + for (PropertyDescriptor pd : + Introspector.getBeanInfo(beanClass, Object.class).getPropertyDescriptors()) { + if (pd.getWriteMethod() != null) { + m.put(pd.getName(), pd); + } + } + return m; + } catch (java.beans.IntrospectionException e) { + throw new AssertionError("Introspector failed on " + beanClass.getName(), e); + } + } + + /** + * {@code shapeMismatches}: HOCON key matches a bean property of nested + * {@code *Config} type but the HOCON value is not OBJECT — walker cannot + * recurse, and downstream binding would throw {@code WrongType}. + */ + private static final class OrphanCounters { + int total; + int bound; + final Set orphans = new TreeSet<>(); + final Set allowlisted = new TreeSet<>(); + final Set shapeMismatches = new TreeSet<>(); + } + + /** + * Fails when reference.conf has keys under {@code sectionPath} (recursively + * through nested {@code *Config} sub-sections) that the bean cannot bind. + * Recurses into a sub-config when the property type satisfies + * {@link #isRecursiveConfigBean} AND the HOCON value is an OBJECT. + */ + static void assertNoHoconOrphans( + String sectionPath, Class beanClass, Set allowedHoconOrphans) { + Config section = ConfigFactory.defaultReference().getConfig(sectionPath); + OrphanCounters c = walkAndLogHoconOrphans( + sectionPath, section, beanClass, allowedHoconOrphans); + AGGREGATES.hoconKey += c.total; + AGGREGATES.hoconBound += c.bound; + AGGREGATES.hoconAllowlisted += c.allowlisted.size(); + AGGREGATES.beans.add(beanClass); + failOnHoconOrphans(sectionPath, beanClass, c); + } + + /** Overload for meta-tests: walks the supplied Config directly, skips AGGREGATES. */ + static void assertNoHoconOrphans( + String label, Config section, Class beanClass, + Set allowedHoconOrphans) { + OrphanCounters c = walkAndLogHoconOrphans( + label, section, beanClass, allowedHoconOrphans); + failOnHoconOrphans(label, beanClass, c); + } + + private static OrphanCounters walkAndLogHoconOrphans( + String label, Config section, Class beanClass, Set allowed) { + OrphanCounters c = new OrphanCounters(); + walkHoconOrphans(beanClass, section, "", allowed, c); + logger.info("[parity-hocon] {} -> {}: hoconKey={}, bound={}, allowlisted={}{}", + label, beanClass.getSimpleName(), c.total, c.bound, + c.allowlisted.size(), c.allowlisted.isEmpty() ? "" : " " + c.allowlisted); + return c; + } + + private static void failOnHoconOrphans( + String label, Class beanClass, OrphanCounters c) { + if (!c.shapeMismatches.isEmpty()) { + throw new AssertionError( + "reference.conf has " + label + ".* keys whose HOCON value " + + "shape does not match the bean property type (expected OBJECT " + + "for nested *Config bean): " + c.shapeMismatches); + } + if (!c.orphans.isEmpty()) { + throw new AssertionError( + "reference.conf has " + label + ".* keys with no matching " + + beanClass.getSimpleName() + " property (at any nesting level) " + + "and not in allowlist: " + c.orphans); + } + } + + private static void walkHoconOrphans( + Class beanClass, Config section, String prefix, + Set allowed, OrphanCounters c) { + Map props = writablePropertyDescriptors(beanClass); + for (String key : new TreeSet<>(section.root().keySet())) { + c.total++; + String qualified = prefix + key; + PropertyDescriptor pd = props.get(key); + if (pd == null) { + if (allowed.contains(qualified)) { + c.allowlisted.add(qualified); + } else { + c.orphans.add(qualified); + } + continue; + } + c.bound++; + Class type = pd.getPropertyType(); + if (isRecursiveConfigBean(type)) { + ConfigValueType valueType = section.root().get(key).valueType(); + if (valueType != ConfigValueType.OBJECT) { + c.shapeMismatches.add(qualified + " (bean type " + + type.getSimpleName() + " requires OBJECT, got " + valueType + ")"); + continue; + } + walkHoconOrphans(type, section.getConfig(key), qualified + ".", allowed, c); + } + } + } + + /** + * Fails when a writable bean property (reachable from {@code beanClass} + * through nested {@code *Config} recursion) has no HOCON key under + * {@code sectionPath} and is not in {@code allowedBeanOrphans}. + */ + static void assertNoBeanOrphans( + String sectionPath, Class beanClass, Set allowedBeanOrphans) { + Config section = ConfigFactory.defaultReference().getConfig(sectionPath); + OrphanCounters c = walkAndLogBeanOrphans( + sectionPath, section, beanClass, allowedBeanOrphans); + AGGREGATES.beanKey += c.total; + AGGREGATES.beanHasKey += c.bound; + AGGREGATES.beanAllowlisted += c.allowlisted.size(); + AGGREGATES.beans.add(beanClass); + failOnBeanOrphans(sectionPath, beanClass, c); + } + + /** Overload for meta-tests: walks the supplied Config directly, skips AGGREGATES. */ + static void assertNoBeanOrphans( + String label, Config section, Class beanClass, + Set allowedBeanOrphans) { + OrphanCounters c = walkAndLogBeanOrphans( + label, section, beanClass, allowedBeanOrphans); + failOnBeanOrphans(label, beanClass, c); + } + + private static OrphanCounters walkAndLogBeanOrphans( + String label, Config section, Class beanClass, Set allowed) { + OrphanCounters c = new OrphanCounters(); + walkBeanOrphans(beanClass, section, "", allowed, c); + logger.info("[parity-bean] {} -> {}: beanKey={}, hasKey={}, allowlisted={}{}", + label, beanClass.getSimpleName(), c.total, c.bound, + c.allowlisted.size(), c.allowlisted.isEmpty() ? "" : " " + c.allowlisted); + return c; + } + + private static void failOnBeanOrphans( + String label, Class beanClass, OrphanCounters c) { + if (!c.shapeMismatches.isEmpty()) { + throw new AssertionError( + beanClass.getSimpleName() + " has nested *Config properties whose " + + "HOCON value shape under " + label + ".* is not OBJECT: " + + c.shapeMismatches); + } + if (!c.orphans.isEmpty()) { + throw new AssertionError( + beanClass.getSimpleName() + " has properties with no matching " + + label + ".* HOCON key (at any nesting level) " + + "and not in allowlist: " + c.orphans); + } + } + + private static void walkBeanOrphans( + Class beanClass, Config section, String prefix, + Set allowed, OrphanCounters c) { + Set keys = section.root().keySet(); + Map props = writablePropertyDescriptors(beanClass); + for (Map.Entry e : props.entrySet()) { + c.total++; + String name = e.getKey(); + String qualified = prefix + name; + PropertyDescriptor pd = e.getValue(); + if (!keys.contains(name)) { + if (allowed.contains(qualified)) { + c.allowlisted.add(qualified); + } else { + c.orphans.add(qualified); + } + continue; + } + c.bound++; + Class type = pd.getPropertyType(); + if (isRecursiveConfigBean(type)) { + ConfigValueType valueType = section.root().get(name).valueType(); + if (valueType != ConfigValueType.OBJECT) { + c.shapeMismatches.add(qualified + " (bean type " + + type.getSimpleName() + " requires OBJECT, got " + valueType + ")"); + continue; + } + walkBeanOrphans(type, section.getConfig(name), qualified + ".", allowed, c); + } + } + } + + /** Build an immutable allowlist from string literals. */ + static Set allowlist(String... names) { + Set s = new HashSet<>(Arrays.asList(names)); + return Collections.unmodifiableSet(s); + } + + /** + * Fails when an allowlist entry no longer resolves to a live target. Prevents + * allowlist rot: a renamed/removed key/property must drop its grandfathering + * entry in the same PR (cf. Cassandra's PROPERTIES_TO_IGNORE long-term decay). + */ + static void assertAllowlistEntriesAreLive( + String sectionPath, Class beanClass, + Set allowedHoconOrphans, + Set allowedBeanOrphans, + Set allowedDivergent) { + Config section = ConfigFactory.defaultReference().getConfig(sectionPath); + runAllowlistEntriesAreLive(sectionPath, section, beanClass, + allowedHoconOrphans, allowedBeanOrphans, allowedDivergent); + } + + /** Overload for meta-tests: see {@link #assertNoHoconOrphans(String, Config, Class, Set)}. */ + static void assertAllowlistEntriesAreLive( + String label, Config section, Class beanClass, + Set allowedHoconOrphans, + Set allowedBeanOrphans, + Set allowedDivergent) { + runAllowlistEntriesAreLive(label, section, beanClass, + allowedHoconOrphans, allowedBeanOrphans, allowedDivergent); + } + + private static void runAllowlistEntriesAreLive( + String label, Config section, Class beanClass, + Set allowedHoconOrphans, + Set allowedBeanOrphans, + Set allowedDivergent) { + List dead = new ArrayList<>(); + + for (String k : allowedHoconOrphans) { + if (!section.hasPath(k)) { + dead.add("hoconOrphan: " + k + " (no longer in reference.conf[" + label + "])"); + } + } + for (String k : allowedBeanOrphans) { + if (!beanPropertyExists(beanClass, k)) { + dead.add("beanOrphan: " + k + " (no longer a writable property of " + + beanClass.getSimpleName() + ")"); + } + } + for (String k : allowedDivergent) { + boolean hoconLive = section.hasPath(k); + boolean beanLive = beanPropertyExists(beanClass, k); + if (!hoconLive || !beanLive) { + dead.add("divergent: " + k + + " (hocon=" + (hoconLive ? "live" : "dead") + + ", bean=" + (beanLive ? "live" : "dead") + ")"); + } + } + + logger.info("[parity-sweep] {} -> {}: hoconOrphans={}, beanOrphans={}, divergent={}, dead={}", + label, beanClass.getSimpleName(), + allowedHoconOrphans.size(), allowedBeanOrphans.size(), + allowedDivergent.size(), dead.size()); + + if (!dead.isEmpty()) { + throw new AssertionError( + "Dead allowlist entries on " + label + " / " + + beanClass.getSimpleName() + " — drop them or restore the " + + "underlying key/property:\n " + String.join("\n ", dead)); + } + } + + /** True iff dotted {@code qualifiedName} resolves to a writable bean property. */ + private static boolean beanPropertyExists(Class beanClass, String qualifiedName) { + Class cursor = beanClass; + String[] segments = qualifiedName.split("\\."); + for (int i = 0; i < segments.length; i++) { + PropertyDescriptor pd = writablePropertyDescriptors(cursor).get(segments[i]); + if (pd == null) { + return false; + } + if (i == segments.length - 1) { + return true; + } + Class type = pd.getPropertyType(); + if (!isRecursiveConfigBean(type)) { + return false; + } + cursor = type; + } + return true; + } + + /** Sentinel: property type outside the dispatcher matrix — hard failure. */ + private static final Object SKIP = new Object(); + + /** Sentinel: property type is a nested {@code *Config} bean to recurse into. */ + private static final Object RECURSE = new Object(); + + /** + * Asserts every writable bean property has a default value equal to its + * reference.conf value. Supported scalar types: {@code int / long / boolean / + * double / float} (and boxed forms), {@code String}, {@code List}. Nested + * {@code *Config} beans are recursed into and matched by dotted name. + *

+ * Skipped: properties with no HOCON key at the current scope, and properties + * named in {@code allowedDivergent} (intentional asymmetry). Properties whose + * type isn't in the dispatcher matrix fail — no silent escape; extend the + * dispatcher or (if genuinely uncomparable) re-introduce a per-section + * {@code typeSkip} allowlist. + */ + static void assertDefaultValuesMatch( + String sectionPath, Class beanClass, Set allowedDivergent) { + Config section = ConfigFactory.defaultReference().getConfig(sectionPath); + Counters c = new Counters(); + List mismatches = runDefaultValuesMatch( + sectionPath, section, beanClass, allowedDivergent, c); + + AGGREGATES.defBeanKey += c.total; + AGGREGATES.defMatched += c.matched; + AGGREGATES.defHoconRecursedKey += c.recursed; + AGGREGATES.defSkipAllow += c.skipAllow.size(); + AGGREGATES.defSkipNoKey += c.skipNoKey.size(); + AGGREGATES.beans.add(beanClass); + + failOnDefaultValueMismatches(sectionPath, beanClass, mismatches); + } + + /** Overload for meta-tests: see {@link #assertNoHoconOrphans(String, Config, Class, Set)}. */ + static void assertDefaultValuesMatch( + String label, Config section, Class beanClass, + Set allowedDivergent) { + Counters c = new Counters(); + List mismatches = runDefaultValuesMatch( + label, section, beanClass, allowedDivergent, c); + failOnDefaultValueMismatches(label, beanClass, mismatches); + } + + private static List runDefaultValuesMatch( + String label, Config section, Class beanClass, + Set allowedDivergent, Counters c) { + Object bean; + try { + bean = beanClass.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new AssertionError("cannot instantiate " + beanClass.getName(), e); + } + List mismatches = new ArrayList<>(); + compareBean(beanClass, bean, section, "", allowedDivergent, mismatches, c); + logger.info("[parity-default] {} -> {}: beanKey={}, matched={}, hoconRecursedKey={}, " + + "divergent-allow={}{}, skip-no-key={}{}", + label, beanClass.getSimpleName(), + c.total, c.matched, c.recursed, + c.skipAllow.size(), c.skipAllow.isEmpty() ? "" : " " + c.skipAllow, + c.skipNoKey.size(), c.skipNoKey.isEmpty() ? "" : " " + c.skipNoKey); + return mismatches; + } + + private static void failOnDefaultValueMismatches( + String label, Class beanClass, List mismatches) { + if (!mismatches.isEmpty()) { + throw new AssertionError( + "Default-value drift between " + beanClass.getSimpleName() + + " and reference.conf[" + label + "]:\n " + + String.join("\n ", mismatches)); + } + } + + /** + * Per-walk accounting. Invariant: {@code total == matched + recursed + + * skipAllow.size() + skipNoKey.size() + mismatches.size()}. Adding a loop + * exit without bumping a counter silently hides coverage drift. + */ + private static final class Counters { + int total; + int matched; + int recursed; + final Set skipAllow = new TreeSet<>(); + final Set skipNoKey = new TreeSet<>(); + } + + private static void compareBean( + Class beanClass, Object beanDefault, Config section, String prefix, + Set allowedDivergent, List mismatches, Counters c) { + PropertyDescriptor[] pds; + try { + pds = Introspector.getBeanInfo(beanClass, Object.class).getPropertyDescriptors(); + } catch (java.beans.IntrospectionException e) { + throw new AssertionError(e); + } + for (PropertyDescriptor pd : pds) { + if (pd.getWriteMethod() == null) { + // @Setter(NONE) for manual post-bind reads — orphan checks cover this side. + continue; + } + c.total++; + String name = pd.getName(); + String qualified = prefix + name; + if (pd.getReadMethod() == null) { + // Write-only property: ConfigBeanFactory binds but nothing reads it back. + mismatches.add(qualified + ": bean property is write-only " + + "(setter present, no getter) — default value cannot be verified " + + "and the bound value cannot be observed; add a getter or drop the field"); + continue; + } + if (allowedDivergent.contains(qualified)) { + c.skipAllow.add(qualified); + continue; + } + if (!section.hasPath(name)) { + c.skipNoKey.add(qualified); + continue; + } + Class type = pd.getPropertyType(); + // Shape guard: nested *Config bean expects HOCON OBJECT; surface as a + // clean mismatch instead of letting getConfig(name) throw WrongType. + if (isRecursiveConfigBean(type)) { + ConfigValueType valueType = section.root().get(name).valueType(); + if (valueType != ConfigValueType.OBJECT) { + mismatches.add(qualified + ": bean type " + type.getSimpleName() + + " requires HOCON OBJECT, got " + valueType); + continue; + } + } + Object hoconValue; + try { + hoconValue = readTypedHoconValue(section, name, type); + } catch (RuntimeException e) { + mismatches.add(qualified + ": type-incompatible HOCON value (" + + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); + continue; + } + if (hoconValue == RECURSE) { + c.recursed++; + Object nested; + try { + nested = pd.getReadMethod().invoke(beanDefault); + } catch (ReflectiveOperationException e) { + throw new AssertionError( + "cannot read " + qualified + " on " + beanClass.getName(), e); + } + if (nested == null) { + mismatches.add(qualified + ": nested " + type.getSimpleName() + + " field is null on a freshly-constructed " + beanClass.getSimpleName() + + " — initialize the field inline (= new " + type.getSimpleName() + "())"); + continue; + } + compareBean(type, nested, section.getConfig(name), qualified + ".", + allowedDivergent, mismatches, c); + continue; + } + if (hoconValue == SKIP) { + mismatches.add(qualified + ": Java type " + type.getSimpleName() + + " not in readTypedHoconValue dispatcher — extend the dispatcher, " + + "or re-introduce a per-section typeSkip allowlist if the type " + + "genuinely cannot be value-compared"); + continue; + } + Object actualDefault; + try { + actualDefault = pd.getReadMethod().invoke(beanDefault); + } catch (ReflectiveOperationException e) { + throw new AssertionError( + "cannot read " + qualified + " on " + beanClass.getName(), e); + } + if (!Objects.equals(actualDefault, hoconValue)) { + // Stamp the runtime type on each side so e.g. Integer(10) vs Long(10) + // doesn't look like `bean=10, reference.conf=10`. + mismatches.add(qualified + ": bean=" + format(actualDefault) + + " (" + typeOf(actualDefault) + ")" + + ", reference.conf=" + format(hoconValue) + + " (" + typeOf(hoconValue) + ")"); + continue; + } + c.matched++; + } + } + + /** Type dispatcher. Returns {@link #RECURSE} for nested *Config, {@link #SKIP} otherwise. */ + private static Object readTypedHoconValue(Config cfg, String path, Class type) { + if (type == int.class || type == Integer.class) { + return cfg.getInt(path); + } + if (type == long.class || type == Long.class) { + return cfg.getLong(path); + } + if (type == boolean.class || type == Boolean.class) { + return cfg.getBoolean(path); + } + if (type == double.class || type == Double.class) { + return cfg.getDouble(path); + } + if (type == float.class || type == Float.class) { + return (float) cfg.getDouble(path); + } + if (type == String.class) { + return cfg.getString(path); + } + if (type == List.class) { + return cfg.getList(path).unwrapped(); + } + if (isRecursiveConfigBean(type) && cfg.hasPath(path)) { + return RECURSE; + } + return SKIP; + } + + /** + * Recursion gate: a non-array/enum/interface class under {@code org.tron.*} + * with a default constructor. Keeps the walker inside project-owned beans. + */ + private static boolean isRecursiveConfigBean(Class type) { + if (type.isPrimitive() || type.isArray() || type.isEnum() || type.isInterface()) { + return false; + } + if (!type.getName().startsWith("org.tron.")) { + return false; + } + try { + type.getDeclaredConstructor(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Cross-section accumulators. Bumped by each helper at the end of its work + * (before throwing) so partial coverage is still reflected. + * {@link #logAggregateSummary} emits one summary line per gate plus + * independently-computed reference totals as a sanity check. + */ + private static final class Aggregates { + int hoconKey; + int hoconBound; + int hoconAllowlisted; + int beanKey; + int beanHasKey; + int beanAllowlisted; + int defBeanKey; + int defMatched; + int defHoconRecursedKey; + int defSkipAllow; + int defSkipNoKey; + // root-level bean classes touched by any helper; recursion walks nested *Config on its own. + final Set> beans = new LinkedHashSet<>(); + } + + private static final Aggregates AGGREGATES = new Aggregates(); + + /** Reset accumulators. Call from {@code @BeforeClass} for clean re-runs in the same JVM. */ + static void resetAggregates() { + AGGREGATES.hoconKey = 0; + AGGREGATES.hoconBound = 0; + AGGREGATES.hoconAllowlisted = 0; + AGGREGATES.beanKey = 0; + AGGREGATES.beanHasKey = 0; + AGGREGATES.beanAllowlisted = 0; + AGGREGATES.defBeanKey = 0; + AGGREGATES.defMatched = 0; + AGGREGATES.defHoconRecursedKey = 0; + AGGREGATES.defSkipAllow = 0; + AGGREGATES.defSkipNoKey = 0; + AGGREGATES.beans.clear(); + } + + /** + * Emit per-gate totals + file-coverage alignment + * {@code file-hoconKey == checkSection + cantCheckSection} and bean-tree + * alignment across {@code parity-bean} / {@code parity-default} / the + * independently-counted registry total. Reviewers can sum columns visually + * to spot a walker that silently skipped a property. + * + * @param checkSectionTopLevels top-level keys hosting a registered Section + * @param cantCheckSectionTopLevels remaining top-level keys (out of parity scope) + */ + static void logAggregateSummary( + Set checkSectionTopLevels, + Set cantCheckSectionTopLevels) { + ConfigObject refRoot = ConfigFactory.parseResources("reference.conf").root(); + int hoconKeyInFile = countHoconKeysRecursive(refRoot); + int checkSectionKey = sumTopLevelSubtreeSize(refRoot, checkSectionTopLevels); + int cantCheckSectionKey = sumTopLevelSubtreeSize(refRoot, cantCheckSectionTopLevels); + + int beanKeyInRegistry = 0; + for (Class b : AGGREGATES.beans) { + beanKeyInRegistry += countBeanSettersRecursive(b); + } + + logger.info("[parity-summary] parity-hocon : hoconKey={}, bound={}, allowlisted={}", + AGGREGATES.hoconKey, AGGREGATES.hoconBound, AGGREGATES.hoconAllowlisted); + logger.info("[parity-summary] parity-bean : beanKey={}, hasKey={}, allowlisted={}", + AGGREGATES.beanKey, AGGREGATES.beanHasKey, AGGREGATES.beanAllowlisted); + logger.info("[parity-summary] parity-default: beanKey={}, matched={}, hoconRecursedKey={}, " + + "divergent-allow={}, skip-no-key={}", + AGGREGATES.defBeanKey, AGGREGATES.defMatched, AGGREGATES.defHoconRecursedKey, + AGGREGATES.defSkipAllow, AGGREGATES.defSkipNoKey); + logger.info("[parity-summary] checkSection {} top-levels {}: hoconKey={} " + + "(= parity-hocon-walked({}) + path-segments-and-internal({}))", + checkSectionTopLevels.size(), checkSectionTopLevels, checkSectionKey, + AGGREGATES.hoconKey, checkSectionKey - AGGREGATES.hoconKey); + logger.info("[parity-summary] cantCheckSection {} top-levels {}: hoconKey={} " + + "(validation skipped; not in checkSection scope)", + cantCheckSectionTopLevels.size(), cantCheckSectionTopLevels, + cantCheckSectionKey); + logger.info("[parity-summary] hocon-align : file-hoconKey({}) = " + + "checkSection({}) + cantCheckSection({})", + hoconKeyInFile, checkSectionKey, cantCheckSectionKey); + logger.info("[parity-summary] bean-align : registry-beanKey({}, across {} bean classes) " + + "= parity-bean({}) = parity-default({})", + beanKeyInRegistry, AGGREGATES.beans.size(), + AGGREGATES.beanKey, AGGREGATES.defBeanKey); + } + + private static int sumTopLevelSubtreeSize(ConfigObject refRoot, Set topLevelKeys) { + int n = 0; + for (String k : topLevelKeys) { + if (!refRoot.containsKey(k)) { + continue; + } + n++; + ConfigValue v = refRoot.get(k); + if (v.valueType() == ConfigValueType.OBJECT) { + n += countHoconKeysRecursive((ConfigObject) v); + } + } + return n; + } + + private static int countHoconKeysRecursive(ConfigObject obj) { + int n = 0; + for (String k : obj.keySet()) { + n++; + ConfigValue v = obj.get(k); + if (v.valueType() == ConfigValueType.OBJECT) { + n += countHoconKeysRecursive((ConfigObject) v); + } + } + return n; + } + + private static int countBeanSettersRecursive(Class beanClass) { + int n = 0; + for (PropertyDescriptor pd : writablePropertyDescriptors(beanClass).values()) { + n++; + Class t = pd.getPropertyType(); + if (isRecursiveConfigBean(t)) { + n += countBeanSettersRecursive(t); + } + } + return n; + } + + private static String typeOf(Object o) { + return o == null ? "null" : o.getClass().getSimpleName(); + } + + private static String format(Object o) { + if (o == null) { + return "null"; + } + if (o instanceof String) { + return "\"" + o + "\""; + } + if (o instanceof List) { + List list = (List) o; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(format(list.get(i))); + } + sb.append("]"); + return sb.toString(); + } + return String.valueOf(o); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java b/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java new file mode 100644 index 0000000000..67b02e556a --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java @@ -0,0 +1,238 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Build-time gate that pins every (section, bean) tuple in {@link #SECTIONS} + * so the entire reference.conf <-> {@code *Config} contract is managed in + * one place. Drift fails the build at PR time instead of waiting for + * {@code ConfigBeanFactory} to throw at process startup. + *

+ * Per-section {@code *ConfigTest} files cover behavioural tests (defaults, + * clamps, alias fallbacks); they do not own parity. Adding a new + * {@code *Config} bean: add a {@link Section} entry below. Adding an + * allowlist entry: include an inline rationale comment; new keys are + * expected to bind 1:1 via {@code ConfigBeanFactory} without exception. + */ +public class ConfigParityGateTest { + + private static final class Section { + final String path; + final Class bean; + final Set hoconOrphans; + final Set beanOrphans; + final Set divergent; + + Section(String path, Class bean, + Set hoconOrphans, Set beanOrphans, + Set divergent) { + this.path = path; + this.bean = bean; + this.hoconOrphans = hoconOrphans; + this.beanOrphans = beanOrphans; + this.divergent = divergent; + } + } + + // legacy acronym casing; normalizeNonStandardKeys renames PBFT -> Pbft before bind + private static final Set COMMITTEE_HOCON_ORPHANS = + ConfigParityCheck.allowlist( + "allowPBFT", + "pBFTExpireNum" + ); + + private static final Set COMMITTEE_BEAN_ORPHANS = + ConfigParityCheck.allowlist( + "allowPbft", // bound from HOCON allowPBFT via normalize hook + "pbftExpireNum" // bound from HOCON pBFTExpireNum via normalize hook + ); + + // native: Java reserved word; bound to bean field nativeQueue, read manually after bind. + // topics: list items have optional fields; EventConfig binds the list manually with + // TOPIC_DEFAULTS fallback (field uses @Setter(NONE)). + private static final Set EVENT_HOCON_ORPHANS = + ConfigParityCheck.allowlist( + "native", + "topics" + ); + + // FilterConfig: reference.conf ships [""] as a schema placeholder so operators see + // the expected element type; bean default is [] (genuinely empty). Both mean "no filter". + private static final Set EVENT_DIVERGENT_DEFAULTS = + ConfigParityCheck.allowlist( + "filter.contractAddress", // bean=[] vs reference.conf=[""] schema placeholder + "filter.contractTopic" // bean=[] vs reference.conf=[""] schema placeholder + ); + + // Genesis fields are mainnet seed data with no sensible in-Java default. + private static final Set GENESIS_DIVERGENT_DEFAULTS = + ConfigParityCheck.allowlist( + "timestamp", // mainnet genesis timestamp, no in-Java default + "parentHash", // mainnet genesis parentHash, no in-Java default + "assets", // seed accounts (Zion / Sun / Blackhole); bean ships empty list + "witnesses" // 27 standby witness nodes; bean ships empty list + ); + + private static final Set NODE_HOCON_ORPHANS = + ConfigParityCheck.allowlist( + "isOpenFullTcpDisconnect", // normalized to bean field openFullTcpDisconnect + "metrics" // delegated to MetricsConfig.fromConfig + ); + + private static final Set NODE_BEAN_ORPHANS = + ConfigParityCheck.allowlist( + "openFullTcpDisconnect" // HOCON ships isOpenFullTcpDisconnect; renamed + ); + + private static final Set NODE_DIVERGENT_DEFAULTS = + ConfigParityCheck.allowlist( + "fastForward" // seed node list, no Java-side default + ); + + // Top-level meta-gate: every reference.conf top-level key must be covered by a + // Section entry above or listed here with a rationale. Closes the "new section + // sneaks in" hole. See everyReferenceConfTopLevelKeyIsCovered. + private static final Set TOP_LEVEL_NON_BEAN = + ConfigParityCheck.allowlist( + "crypto", // MiscConfig.cryptoEngine manual-read root + "enery", // MiscConfig manual-read root (preserves historical typo of "energy") + "localwitness", // bound by LocalWitnessConfig, not in the *ConfigBean factory pattern + "net", // deprecated wrapper for net.type; intentionally empty in reference.conf + "seed", // MiscConfig.seedNodeIpList manual-read root (seed.node.ip.list) + "trx" // MiscConfig.trxReferenceBlock manual-read root (trx.reference.block) + ); + + private static final List

SECTIONS; + + static { + Set empty = Collections.emptySet(); + List
s = new ArrayList<>(); + // ctor args: (path, beanClass, hoconOrphans, beanOrphans, divergent) + s.add(new Section("block", BlockConfig.class, + empty, empty, empty)); + s.add(new Section("committee", CommitteeConfig.class, + COMMITTEE_HOCON_ORPHANS, COMMITTEE_BEAN_ORPHANS, empty)); + s.add(new Section("event.subscribe", EventConfig.class, + EVENT_HOCON_ORPHANS, empty, EVENT_DIVERGENT_DEFAULTS)); + s.add(new Section("genesis.block", GenesisConfig.class, + empty, empty, GENESIS_DIVERGENT_DEFAULTS)); + s.add(new Section("node", NodeConfig.class, + NODE_HOCON_ORPHANS, NODE_BEAN_ORPHANS, NODE_DIVERGENT_DEFAULTS)); + s.add(new Section("node.metrics", MetricsConfig.class, + empty, empty, empty)); + s.add(new Section("rate.limiter", RateLimiterConfig.class, + empty, empty, empty)); + s.add(new Section("storage", StorageConfig.class, + empty, empty, empty)); + s.add(new Section("vm", VmConfig.class, + empty, empty, empty)); + SECTIONS = Collections.unmodifiableList(s); + } + + @BeforeClass + public static void resetAggregates() { + ConfigParityCheck.resetAggregates(); + } + + /** Emit cross-section [parity-summary] totals + file-coverage alignment. */ + @AfterClass + public static void logAggregateSummary() { + Set checkSectionTopLevels = new TreeSet<>(); + for (Section s : SECTIONS) { + checkSectionTopLevels.add(s.path.split("\\.", 2)[0]); + } + ConfigParityCheck.logAggregateSummary( + checkSectionTopLevels, TOP_LEVEL_NON_BEAN); + } + + @Test + public void hoconKeysAreBound() { + for (Section s : SECTIONS) { + ConfigParityCheck.assertNoHoconOrphans(s.path, s.bean, s.hoconOrphans); + } + } + + @Test + public void beanPropertiesHaveHoconKeys() { + for (Section s : SECTIONS) { + ConfigParityCheck.assertNoBeanOrphans(s.path, s.bean, s.beanOrphans); + } + } + + @Test + public void defaultValuesMatch() { + List failures = new ArrayList<>(); + for (Section s : SECTIONS) { + try { + ConfigParityCheck.assertDefaultValuesMatch( + s.path, s.bean, s.divergent); + } catch (AssertionError e) { + failures.add(e.getMessage()); + } + } + if (!failures.isEmpty()) { + throw new AssertionError( + failures.size() + " section(s) failed default-value parity:\n\n" + + String.join("\n\n", failures)); + } + } + + /** + * Fails when any allowlist entry no longer resolves to a live HOCON path or + * bean property — i.e. the underlying key/property was renamed or removed + * but the grandfathering entry was left behind. + */ + @Test + public void allowlistEntriesAreLive() { + List failures = new ArrayList<>(); + for (Section s : SECTIONS) { + try { + ConfigParityCheck.assertAllowlistEntriesAreLive( + s.path, s.bean, s.hoconOrphans, s.beanOrphans, s.divergent); + } catch (AssertionError e) { + failures.add(e.getMessage()); + } + } + if (!failures.isEmpty()) { + throw new AssertionError( + failures.size() + " section(s) have dead allowlist entries:\n\n" + + String.join("\n\n", failures)); + } + } + + /** + * Fails when reference.conf grows a top-level key not covered by a Section + * or {@link #TOP_LEVEL_NON_BEAN}. Uses {@code parseResources} so JVM system + * properties don't pollute the top-level key set. + */ + @Test + public void everyReferenceConfTopLevelKeyIsCovered() { + Config refFile = ConfigFactory.parseResources("reference.conf"); + Set topKeys = new TreeSet<>(refFile.root().keySet()); + Set covered = new TreeSet<>(); + for (Section s : SECTIONS) { + covered.add(s.path.split("\\.", 2)[0]); + } + covered.addAll(TOP_LEVEL_NON_BEAN); + + Set orphans = new TreeSet<>(topKeys); + orphans.removeAll(covered); + if (!orphans.isEmpty()) { + throw new AssertionError( + "reference.conf has top-level keys not covered by SECTIONS and not in " + + "TOP_LEVEL_NON_BEAN: " + orphans + + ". Either add a new Section entry (preferred — auto-binds via " + + "*Config bean) or register the key under TOP_LEVEL_NON_BEAN with " + + "an inline rationale."); + } + } +} From c076a72b791b91cb39bbfc993eceae58c586a157 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 11:21:06 +0800 Subject: [PATCH 09/13] docs(conf): fix inaccurate comments and restructure block-opener comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on PR #6810: - blockCacheTimeout: correct unit from (s) to (min) — both SyncService and AdvService pass the value to expireAfterWrite with TimeUnit.MINUTES - pBFTExpireNum: reword from "maintenance rounds" to block-lag count; PbftMsgHandler compares headBlockNum - msg.getNumber() against it - allowReceiptsMerkleRoot: mark as reserved/no-op; Args.java explicitly skips applying it at runtime - allowTvmBlob: narrow from "blob transaction support" to the actual effect: enabling BLOBHASH and BLOBBASEFEE opcodes in TVM - committee section header: soften — not every entry is a committee proposal (allowNewRewardAlgorithm is a startup flag) - event.subscribe block: move inline comments on { / [ openers to the preceding line per style feedback - contractAddress / contractTopic: remove duplicate element-level comments; keep the array-level comment only --- common/src/main/resources/reference.conf | 39 +++++++++++++----------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 1824afb9d0..0dc7b383ba 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -343,7 +343,7 @@ node { # Block solidification check unsolidifiedBlockCheck = false maxUnsolidifiedBlocks = 54 # Maximum unsolidified blocks allowed,when accept transaction - blockCacheTimeout = 60 # Block cache timeout (s) in P2P service + blockCacheTimeout = 60 # Block cache timeout (min) in P2P service # TCP and transaction limits maxTransactionPendingSize = 2000 @@ -797,11 +797,10 @@ vm = { # TVM execution settings. constantCallTimeoutMs = 0 } -# Governance parameters exposed by /wallet/getchainparameters. -# Comments list the API chainParameter key and ProposalType ID where applicable. -# All default to 0 (disabled) unless noted. -# Controlled by on-chain committee proposals, not manual configuration. -# Setting them in config is only for private chain testing. +# Governance / feature-flag parameters. Most are controlled by on-chain committee proposals; +# a few (e.g. allowNewRewardAlgorithm) are startup-only flags. +# Comments list the /wallet/getchainparameters API key and ProposalType ID where applicable. +# All default to 0 (disabled) unless noted. Manual config is for private-chain testing only. committee = { allowCreationOfContracts = 0 # getAllowCreationOfContracts, #9: enable smart contract creation allowMultiSign = 0 # getAllowMultiSign, #20: enable account permission multi-signature @@ -819,11 +818,11 @@ committee = { allowAccountStateRoot = 0 # getAllowAccountStateRoot, #25: enable account state root changedDelegation = 0 # getChangeDelegation, #30: enable delegation changes allowPBFT = 0 # getAllowPBFT, #40: enable PBFT consensus - pBFTExpireNum = 20 # PBFT message expiration, in maintenance rounds + pBFTExpireNum = 20 # PBFT message expiration: drop BLOCK messages whose block number lags head by more than this many blocks allowTransactionFeePool = 0 # getAllowTransactionFeePool, #48: enable transaction fee pool allowBlackHoleOptimization = 0 # getAllowOptimizeBlackHole, #49: enable blackhole account optimization allowNewResourceModel = 0 # getAllowNewResourceModel, #51: enable new resource model - allowReceiptsMerkleRoot = 0 # receiptsMerkleRoot: enable receipts Merkle root + allowReceiptsMerkleRoot = 0 # receiptsMerkleRoot: receipts Merkle root (not yet applied at runtime) allowTvmFreeze = 0 # getAllowTvmFreeze, #52: enable freeze operations in TVM allowTvmVote = 0 # getAllowTvmVote, #59: enable vote operations in TVM unfreezeDelayDays = 0 # getUnfreezeDelayDays, #70: resource unfreeze delay days [1, 365] @@ -838,7 +837,7 @@ committee = { allowStrictMath = 0 # getAllowStrictMath, #87: enable strict arithmetic checks consensusLogicOptimization = 0 # getConsensusLogicOptimization, #88: enable consensus logic optimization allowTvmCancun = 0 # getAllowTvmCancun, #83: enable Cancun TVM rules - allowTvmBlob = 0 # getAllowTvmBlob, #89: enable blob transaction support in TVM + allowTvmBlob = 0 # getAllowTvmBlob, #89: enable blob-related TVM opcodes (BLOBHASH, BLOBBASEFEE) allowAccountAssetOptimization = 0 # getAllowAccountAssetOptimization, #53: enable account asset optimization allowAssetOptimization = 0 # getAllowAssetOptimization, #66: enable asset optimization allowNewReward = 0 # getAllowNewReward, #67: enable new reward logic @@ -850,10 +849,12 @@ committee = { dynamicEnergyMaxFactor = 0 # getDynamicEnergyMaxFactor, #75: maximum dynamic energy factor } -event.subscribe = { # Event subscription settings. +# Event subscription settings. +event.subscribe = { enable = false # Whether to enable event subscription. - native = { # Native event queue settings. + # Native event queue settings. + native = { useNativeQueue = false // if true, use native message queue, else use event plugin. bindport = 5555 // bind port sendqueuelength = 1000 // max length of send queue @@ -870,7 +871,8 @@ event.subscribe = { # Event subscription settings. dbconfig = "" contractParse = true # Whether to parse contract event data. - topics = [ # Event trigger topics. + # Event trigger topics. + topics = [ { triggerName = "block" // block trigger, the value can't be modified enable = false // Whether to enable this trigger. @@ -915,14 +917,17 @@ event.subscribe = { # Event subscription settings. } ] - filter = { # Event filter settings. + # Event filter settings. + filter = { fromblock = "" // "", "earliest", or a specific block number as the beginning of the queried range toblock = "" // "", "latest", or a specific block number as end of the queried range - contractAddress = [ // Contract addresses to subscribe; "" means any contract address. - "" // contract address to subscribe; "" means any contract address + // Contract addresses to subscribe; "" means any contract address. + contractAddress = [ + "" ] - contractTopic = [ // Contract topics to subscribe; "" means any contract topic. - "" // contract topic to subscribe; "" means any contract topic + // Contract topics to subscribe; "" means any contract topic. + contractTopic = [ + "" ] } } From cd5e11635975e53957922a4986269e4c5743e33b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 11:48:24 +0800 Subject: [PATCH 10/13] test(config): allowlist storage.properties in parity gate StorageConfig.properties is List and is parsed manually via StorageConfig.readProperties(); ConfigBeanFactory cannot bind list-of-object fields so the hoconKeysAreBound gate reports it as an orphan. Add it to STORAGE_HOCON_ORPHANS with the rationale comment. --- .../org/tron/core/config/args/ConfigParityGateTest.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java b/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java index 67b02e556a..cbfedb9664 100644 --- a/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java +++ b/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java @@ -82,6 +82,13 @@ private static final class Section { "witnesses" // 27 standby witness nodes; bean ships empty list ); + // properties: List parsed manually via StorageConfig.readProperties(); + // ConfigBeanFactory cannot bind list-of-object fields, so the gate sees it as unbound. + private static final Set STORAGE_HOCON_ORPHANS = + ConfigParityCheck.allowlist( + "properties" // manually parsed by StorageConfig.readProperties() + ); + private static final Set NODE_HOCON_ORPHANS = ConfigParityCheck.allowlist( "isOpenFullTcpDisconnect", // normalized to bean field openFullTcpDisconnect @@ -132,7 +139,7 @@ private static final class Section { s.add(new Section("rate.limiter", RateLimiterConfig.class, empty, empty, empty)); s.add(new Section("storage", StorageConfig.class, - empty, empty, empty)); + STORAGE_HOCON_ORPHANS, empty, empty)); s.add(new Section("vm", VmConfig.class, empty, empty, empty)); SECTIONS = Collections.unmodifiableList(s); From 0041c2bcab48379093b9364b53f53388f874717f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 12:04:37 +0800 Subject: [PATCH 11/13] ci(build): tolerate base-branch test failures in Coverage Base job Merge-order races can leave the base branch with a pre-existing failing test unrelated to the current PR (e.g. #6803 vs #6806 ordering). The Coverage Base job only needs jacoco XML for coverage diffing, so a stale test failure must not block the whole job. Add continue-on-error: true to the Build (base) and Test with RocksDB engine (base) steps, and relax if-no-files-found from error to warn so that a compile-level failure degrades gracefully instead of producing a misleading second failure. --- .github/workflows/pr-build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 191d8be778..f35538c096 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -260,9 +260,15 @@ jobs: coverage-base-x86_64-gradle- - name: Build (base) + # Test failures on the base branch are tolerated: merge-order races can + # leave the base with a pre-existing failing test that is unrelated to + # this PR. The only output we need from this job is the jacoco XML for + # coverage diffing, so we must not let a stale test failure block it. + continue-on-error: true run: ./gradlew clean build --no-daemon --no-build-cache - name: Test with RocksDB engine (base) + continue-on-error: true run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache - name: Generate module coverage reports (base) @@ -274,7 +280,7 @@ jobs: name: jacoco-coverage-base path: | **/build/reports/jacoco/test/jacocoTestReport.xml - if-no-files-found: error + if-no-files-found: warn coverage-gate: name: Coverage Gate From 613f66b12dcb24523d3f866f484ec518e4e25ca9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 14:16:20 +0800 Subject: [PATCH 12/13] docs(conf): move block-opener inline comments to preceding line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All keys whose value opens with { or [ now carry the comment on the line immediately above the key rather than on the same line after the opener. Applies to 16 remaining occurrences: node.backup, members, node.metrics, prometheus, p2p (version), active, passive, fastForward, http, rpc, dns, jsonrpc, rate.limiter.p2p, seed.node, ip.list, localwitness, block, vm. CI gates: check_reference_comments → OK, check_reference_conf → OK (290 keys). --- common/src/main/resources/reference.conf | 54 ++++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 0dc7b383ba..2f8f309760 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -152,11 +152,13 @@ node.discovery = { # BlockCount = 12 # block sync count after node start # } -node.backup { # Backup node election settings. +# Backup node election settings. +node.backup { port = 10001 # UDP listen port; each member should have the same configuration priority = 0 # Node priority; each member should use a different priority keepAliveInterval = 3000 # Keep-alive interval (ms); each member should have the same configuration - members = [ # Backup member IP list. + # Backup member IP list. + members = [ # "ip", # Peer IP list, better to add at most one IP; must not contain this node's own IP ] } @@ -169,8 +171,10 @@ crypto { # Energy limit block number (config key has typo "enery" preserved for backward compatibility) enery.limit.block.num = 4727890 -node.metrics = { # Node metrics settings. - prometheus { # Prometheus exporter settings. +# Node metrics settings. +node.metrics = { + # Prometheus exporter settings. + prometheus { enable = false # Whether to enable Prometheus metrics. port = 9527 # Prometheus exporter port. } @@ -234,28 +238,33 @@ node { # shipping it in reference.conf would always mask the modern `maxConnectionsWithSameIp`. metricsEnable = false - p2p { # P2P protocol version settings. + # P2P protocol version settings. + p2p { version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 } - active = [ # Peers to actively connect to. + # Peers to actively connect to. + active = [ # Active establish connection in any case # "ip:port", # "ip:port" ] - passive = [ # Peers allowed to passively connect. + # Peers allowed to passively connect. + passive = [ # Passive accept connection in any case # "ip:port", # "ip:port" ] - fastForward = [ # Fast-forward peer list. + # Fast-forward peer list. + fastForward = [ "100.27.171.62:18888", "15.188.6.125:18888" ] - http { # HTTP API settings. + # HTTP API settings. + http { fullNodeEnable = true # Whether to enable FullNode HTTP API. fullNodePort = 8090 # FullNode HTTP API port. solidityEnable = true # Whether to enable Solidity HTTP API. @@ -268,7 +277,8 @@ node { maxMessageSize = 4194304 } - rpc { # gRPC API settings. + # gRPC API settings. + rpc { enable = true # Whether to enable FullNode gRPC API. port = 50051 # FullNode gRPC API port. solidityEnable = true # Whether to enable Solidity gRPC API. @@ -361,7 +371,8 @@ node { # Contract proto validation thread pool (0 = auto: availableProcessors) validContractProto.threads = 0 - dns { # DNS discovery and publish settings. + # DNS discovery and publish settings. + dns { # DNS URLs to discover peers, format: tree://{pubkey}@{domain}. Default: empty. treeUrls = [ # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", @@ -403,7 +414,8 @@ node { activeConnectFactor = 0.1 connectFactor = 0.6 # Deprecated connection factor. - jsonrpc { # JSON-RPC API settings. + # JSON-RPC API settings. + jsonrpc { # Note: Before release_4.8.1, if you turn on jsonrpc and run it for a while and then turn it off, # you will not be able to get the data from eth_getLogs for that period of time. Default: false httpFullNodeEnable = false @@ -489,7 +501,8 @@ rate.limiter = { # } ] - p2p = { # P2P message rate limits. + # P2P message rate limits. + p2p = { # QPS ceiling for individual P2P message types received from peers. # Values are doubles; fractional QPS is allowed (e.g. 0.5 = one per 2 s). syncBlockChain = 3.0 # SyncBlockChain handshake messages @@ -507,8 +520,10 @@ rate.limiter = { apiNonBlocking = false } -seed.node = { # Bootstrap seed node settings. - ip.list = [ # Seed node addresses. +# Bootstrap seed node settings. +seed.node = { + # Seed node addresses. + ip.list = [ "3.225.171.164:18888", "52.8.46.215:18888", "3.79.71.167:18888", @@ -740,14 +755,16 @@ genesis.block = { # When it is empty,the localwitness is configured with the private key of the witness account. # localWitnessAccountAddress = -localwitness = [ # Local witness private keys. +# Local witness private keys. +localwitness = [ ] # localwitnesskeystore = [ # "localwitnesskeystore.json" # ] -block = { # Block processing settings. +# Block processing settings. +block = { needSyncCheck = false // Whether to check sync before producing blocks. maintenanceTimeInterval = 21600000 // 6 hours (ms) proposalExpireTime = 259200000 // 3 days (ms), controlled by committee proposal @@ -760,7 +777,8 @@ trx.reference.block = "solid" # Transaction expiration time in milliseconds. trx.expiration.timeInMilliseconds = 60000 -vm = { # TVM execution settings. +# TVM execution settings. +vm = { supportConstant = false # Whether to support constant contract calls. maxEnergyLimitForConstant = 100000000 # Max energy for constant calls. minTimeRatio = 0.0 # Minimum VM time ratio. From 170c7dcb03afd4f0a92df99ca14f8fdf58e64309 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 5 Jun 2026 14:41:37 +0800 Subject: [PATCH 13/13] docs(conf): fix comment inaccuracies (db.directory, checkpoint.sync, passive, maxFastForwardNum) - db.directory: reference CLI flag --output-directory explicitly - checkpoint.sync: note it is ignored under checkpoint.version = 1 (default); only CheckPointV2Store.java:19 consumes it - P2P service: add missing space after comma - maxFastForwardNum: reword to describe SR-forwarding semantics, verified via RelayService.java:234 getNextWitnesses() - passive: note port numbers are configured but ignored; Args.java:657 only extracts InetAddress, discarding the port - check_reference_comments.py: add encoding="utf-8" to read_text() --- .github/scripts/check_reference_comments.py | 2 +- common/src/main/resources/reference.conf | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/check_reference_comments.py b/.github/scripts/check_reference_comments.py index 86055dcf01..0dd32f391b 100644 --- a/.github/scripts/check_reference_comments.py +++ b/.github/scripts/check_reference_comments.py @@ -204,7 +204,7 @@ def collect_keys(path, list_all=False): *list_all* is True (``--list`` flag); always empty otherwise. status is one of: "commented" | "dedup" | "missing". """ - lines = path.read_text().splitlines() + lines = path.read_text(encoding="utf-8").splitlines() # stack — bracket-nesting context, one frame per open { or [. # Each frame is a dict: diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 2f8f309760..f1e4907274 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -45,7 +45,7 @@ storage { # Asynchronous writes can significantly improve FullNode block sync performance. db.sync = false - db.directory = "database" # Database directory under the node output path. + db.directory = "database" # Database directory under the node --output-directory path. # Whether to write transaction result in transactionRetStore transHistory.switch = "on" @@ -121,7 +121,7 @@ storage { # Checkpoint version for snapshot mechanism. Version 2 enables V2 snapshot. checkpoint.version = 1 - checkpoint.sync = true # Whether to sync when write checkpoint storage. + checkpoint.sync = true # Sync flag for V2 checkpoint writes; ignored when checkpoint.version = 1 (default). # Estimated number of block transactions (default 1000, min 100, max 10000). # Total cached transactions = 65536 * txCache.estimatedTransactions @@ -228,10 +228,10 @@ node { # Max block inv hashes accepted per peer per second. Minimum: 1. maxBlockInvPerSecond = 10 - # In P2P service,whether any peer connection can be disconnected, when peers connection over maxConnections. + # In P2P service, whether any peer connection can be disconnected, when peers connection over maxConnections. isOpenFullTcpDisconnect = false inactiveThreshold = 600 // seconds - maxFastForwardNum = 4 # Maximum fast-forward peers. + maxFastForwardNum = 4 # Number of SRs after the block-producing SR that a fast-forward node forwards blocks to. # Legacy alias `maxActiveNodesWithSameIp` is still accepted from user config # (see NodeConfig alias-fallback) but is intentionally NOT defaulted here — @@ -250,7 +250,7 @@ node { # "ip:port" ] - # Peers allowed to passively connect. + # List of IP addresses from which incoming connections are accepted; port numbers are configured but ignored. passive = [ # Passive accept connection in any case # "ip:port",