diff --git a/.github/scripts/check_reference_comments.py b/.github/scripts/check_reference_comments.py new file mode 100644 index 00000000000..0dd32f391bc --- /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(encoding="utf-8").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/integration-test-multinode.yml b/.github/workflows/integration-test-multinode.yml new file mode 100644 index 00000000000..fadfc2168d2 --- /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@v5 + 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 00000000000..b0c10247a7f --- /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@v5 + 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-build.yml b/.github/workflows/pr-build.yml index dd005f98b74..f35538c0961 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 @@ -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 diff --git a/.github/workflows/pr-cancel.yml b/.github/workflows/pr-cancel.yml index bbd0e68c235..3213026d3f9 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/pr-check.yml b/.github/workflows/pr-check.yml index 7ae169a8690..506a823a4f7 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: @@ -126,7 +132,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/system-test.yml b/.github/workflows/system-test.yml deleted file mode 100644 index f6184fb0efc..00000000000 --- 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 diff --git a/README.md b/README.md index 575409b3a96..be84b44150b 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/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 0dc8fb31ada..3993e8ed835 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 3ed968e1afa..41822df2391 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 c94f28b3a2f..e07360e6863 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/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java index 63acf64b64f..e6cbd52e595 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 94d22f4b474..cb6f299e872 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/common/src/main/java/org/tron/core/config/Parameter.java b/common/src/main/java/org/tron/core/config/Parameter.java index 5349ef8d875..233f1d9ef7a 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/common/src/main/java/org/tron/core/config/README.md b/common/src/main/java/org/tron/core/config/README.md index c34994519d9..1380c98984e 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 f1317e04914..16dd8295be1 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 e8823d81984..2c6c3e60a41 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 549e280bbe1..f1e4907274f 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -25,93 +25,103 @@ # 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. -# # ============================================================================= +# 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) + # 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" + + db.directory = "database" # Database directory under the node --output-directory path. # 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 = [] - 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 # 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 @@ -128,10 +138,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,10 +152,12 @@ node.discovery = { # BlockCount = 12 # block sync count after node start # } +# 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 + # Backup member IP list. members = [ # "ip", # Peer IP list, better to add at most one IP; must not contain this node's own IP ] @@ -152,19 +165,22 @@ node.backup { # 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 settings. node.metrics = { + # Prometheus exporter settings. prometheus { - enable = false - port = 9527 + 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 +188,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 +200,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 +228,63 @@ 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 # 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 — # shipping it in reference.conf would always mask the modern `maxConnectionsWithSameIp`. metricsEnable = false + # P2P protocol version settings. p2p { version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 } + # Peers to actively connect to. active = [ # Active establish connection in any case # "ip:port", # "ip:port" ] + # 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", # "ip:port" ] + # Fast-forward peer list. fastForward = [ "100.27.171.62:18888", "15.188.6.125:18888" ] + # HTTP API settings. http { - fullNodeEnable = true - fullNodePort = 8090 - solidityEnable = true - solidityPort = 8091 - PBFTEnable = true - PBFTPort = 8092 + 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 } + # gRPC API settings. rpc { - enable = true - port = 50051 - solidityEnable = true - solidityPort = 50061 - PBFTEnable = true - PBFTPort = 50071 + 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 +320,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 +346,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 (min) 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,11 +366,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 discovery and publish settings. dns { # DNS URLs to discover peers, format: tree://{pubkey}@{domain}. Default: empty. treeUrls = [ @@ -388,17 +412,18 @@ 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. + # 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 - 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,6 +501,7 @@ rate.limiter = { # } ] + # 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). @@ -494,7 +520,9 @@ rate.limiter = { apiNonBlocking = false } +# Bootstrap seed node settings. seed.node = { + # Seed node addresses. ip.list = [ "3.225.171.164:18888", "52.8.46.215:18888", @@ -548,10 +576,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 +603,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,6 +755,7 @@ genesis.block = { # When it is empty,the localwitness is configured with the private key of the witness account. # localWitnessAccountAddress = +# Local witness private keys. localwitness = [ ] @@ -734,8 +763,9 @@ localwitness = [ # "localwitnesskeystore.json" # ] +# Block processing settings. block = { - needSyncCheck = false + 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 +777,15 @@ trx.reference.block = "solid" # Transaction expiration time in milliseconds. trx.expiration.timeInMilliseconds = 60000 +# TVM execution settings. vm = { - supportConstant = false - maxEnergyLimitForConstant = 100000000 - minTimeRatio = 0.0 - maxTimeRatio = 5.0 - saveInternalTx = false - lruCacheSize = 500 - vmTrace = false + 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 +815,70 @@ vm = { constantCallTimeoutMs = 0 } -# Governance proposal toggle parameters. All default to 0 (disabled). -# 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 - 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: 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: 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] + 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-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 + 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 subscription settings. event.subscribe = { - enable = false + enable = false # Whether to enable event subscription. + # 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 } - 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 +887,13 @@ 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. + # Event trigger topics. 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 +902,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,14 +935,17 @@ event.subscribe = { } ] + # 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 + // Contract addresses to subscribe; "" means any contract address. contractAddress = [ - "" // contract address to subscribe; "" means any contract address + "" ] + // Contract topics to subscribe; "" means any contract topic. contractTopic = [ - "" // contract topic to subscribe; "" means any contract topic + "" ] } } 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 00000000000..051ebeaef00 --- /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 00000000000..cbfedb96643 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/ConfigParityGateTest.java @@ -0,0 +1,245 @@ +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 + ); + + // 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 + "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, + STORAGE_HOCON_ORPHANS, 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."); + } + } +} 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 d8700880cd0..e3f1925a763 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/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 5f1540b672e..23050f5218d 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 9dbe92fb78e..beb9ede2e14 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/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index 3ce7ce20312..73e4f1e826b 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 00000000000..6fbecb4c87c --- /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); + } +} 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 88e95f9653e..c9fea6bce45 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()); + }); } } 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 3c00c6ea00e..c6b954838ca 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/java/org/tron/core/zksnark/ShieldedReceiveTest.java b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java index 0d14d6fbc26..5854b731e97 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 7842eed8484..ade00374bc4 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() ───────────────────────────────────────────────── /** diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 2277346234b..a7bf77654cb 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 # }, ]