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/scripts/check_reference_conf.py b/.github/scripts/check_reference_conf.py new file mode 100644 index 00000000000..d9e2f3f20cf --- /dev/null +++ b/.github/scripts/check_reference_conf.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +"""Validate java-tron reference.conf key names and hierarchy depth. + +Rules enforced: + 1. Every user-defined segment of every key path must match ^[a-z][a-zA-Z0-9]*$: + starts with a lowercase ASCII letter, then ASCII letters/digits only. + Acronyms at position 1+ are accepted (e.g. `httpPBFTEnable`, + `openHistoryQueryWhenLiteFN`, `allowShieldedTRC20Transaction`) — only the + first character is constrained. This matches what java.beans.Introspector + and ConfigBeanFactory actually require for bean-property auto-binding. + 2. Total path depth must be <= MAX_DEPTH (5). Each list/array step counts + as one additional level. For example `rate.limiter.http[].component` + is 5 levels deep (rate=1, limiter=2, http=3, []=4, component=5). + 3. ALLOWLIST entries are exempt from the format rule (legacy keys that ship + in user configs; renaming would break compatibility). + 4. Service-binding port values must be unique. A leaf is a "service port" + when its last segment is `port` or ends in `Port` (camelCase) AND its + path contains no `[]` (list-element ports belong to per-element records, + not to the local process). Two distinct paths binding the same int value + would conflict at startup; reserved sentinels (0, -1) are exempt. + +Parsing strategy: delegated to pyhocon (https://github.com/chimpler/pyhocon), +the reference Python HOCON implementation. This avoids hand-rolled scanner +pitfalls (key = { ... } prefix loss, triple-strings, substitutions, includes, ++= operator, block comments). pyhocon returns a fully-merged ConfigTree where +dotted-form keys are expanded into nested objects — i.e. the same canonical +key set Typesafe Config / ConfigBeanFactory will see at runtime. + +Array handling: keys inside object-elements of arrays are also user-defined +config keys (e.g. each entry in `rate.limiter.rpc = [{ component=..., ... }]` +is parsed by RateLimiterConfig). The walker recurses into list elements and +treats the array step as a synthetic `[]` segment that contributes to depth +but is not itself validated as a name. Element keys are deduplicated across +list entries because well-formed arrays use homogeneous object shapes. + +Debug mode: pass `--debug` to print every parsed key with its depth, in +walk order (which mirrors the file top-to-bottom). Use this to eyeball the +parser's view against reference.conf. + +Exit code: 0 if clean, 1 if any violation remains after allowlist filtering, +2 on environment errors (missing pyhocon, file not found, parse failure). + +CI integration: invoked by the `Validate reference.conf key names and depth` +step of the `checkstyle` job in `.github/workflows/pr-check.yml`. The non-zero +exit on violations is what makes that step fail — there is intentionally NO +extra `exit 1` in the workflow shell wrapper. A single GHA `::error` workflow +command is also emitted unconditionally (not gated on the GITHUB_ACTIONS env +var) so local runs produce the same output as CI; the leading `::` is +harmless noise locally. +""" +import re +import sys +from pathlib import Path + +try: + from pyhocon import ConfigFactory, ConfigTree +except ImportError: + print( + "error: pyhocon is required. Install with `pip install pyhocon`.", + file=sys.stderr, + ) + sys.exit(2) + +# Set at the current max depth of reference.conf (5). No buffer: a mature +# project should not allow silent drift, so any new key going deeper must +# bump MAX_DEPTH via an explicit, reviewed change (deeper trees hurt +# readability and complicate ConfigBeanFactory mapping). +MAX_DEPTH = 5 +KEY_REGEX = re.compile(r'^[a-z][a-zA-Z0-9]*$') +# Legacy keys grandfathered to keep user `config.conf` files compatible. +# Do NOT extend this list for new keys — every new key must satisfy KEY_REGEX. +# A future rename + deprecation cycle can shrink this set back to empty. +ALLOWLIST = { + # PBFT acronym in capitals — predates the auto-binding convention. + "node.http.PBFTEnable", + "node.http.PBFTPort", + "node.rpc.PBFTEnable", + "node.rpc.PBFTPort", + # PascalCase exceptions handled manually in NodeConfig.fromConfig (not via + # ConfigBeanFactory). Currently commented out in reference.conf, so the + # parser does not see them today — listed here so the gate stays green if + # a future change uncomments them with defaults. + "node.shutdown.BlockTime", + "node.shutdown.BlockHeight", + "node.shutdown.BlockCount", +} + +# Sentinel port values exempt from the uniqueness check. 0 = disabled (the +# service does not bind); -1 = auto/unset placeholder. Any number of leaves +# may share these values. +PORT_SENTINELS = {0, -1} + + +def walk(node, path, depth): + """Yield (full_path, depth, is_leaf) for every reachable user-defined key. + + - ConfigTree key adds one depth level and contributes a name segment. + - list step adds one synthetic level rendered as `[]`. Element-internal + keys are walked once per unique sub-path (homogeneous object arrays + otherwise yield each field N times). + - Scalars / null / list-of-scalars produce no further keys. + + `depth` includes the array `[]` steps. `is_leaf` is True when the value + at this path is a scalar/list/null — i.e. not another ConfigTree — so + callers can filter leaves vs namespace intermediates. + """ + if isinstance(node, ConfigTree): + for k, v in node.items(): + new_path = f"{path}.{k}" if path else k + new_depth = depth + 1 + is_leaf = not isinstance(v, ConfigTree) + yield new_path, new_depth, is_leaf + yield from walk(v, new_path, new_depth) + elif isinstance(node, list): + array_path = f"{path}[]" + array_depth = depth + 1 + seen = set() + for elem in node: + # Object element: walk its keys. Nested list element (HOCON allows + # list-of-list, e.g. `a = [[{x=1}]]`): recurse so each inner [] step + # also contributes to depth. Scalar elements have no sub-keys. + if isinstance(elem, (ConfigTree, list)): + for sub_path, sub_depth, sub_leaf in walk(elem, array_path, array_depth): + if sub_path in seen: + continue + seen.add(sub_path) + yield sub_path, sub_depth, sub_leaf + + +def _is_port_segment(seg): + """Last-segment test for a service-binding port leaf. + + Matches `port` (exact) and any camelCase form ending in `Port` + (e.g. `fullNodePort`, `solidityPort`, `PBFTPort`). Deliberately rejects + lowercase `port` as a suffix inside a longer word (`transport`, + `support`) — those are not port keys. + """ + return seg == "port" or seg.endswith("Port") + + +def find_port_collisions(tree, keys): + """Group service-binding port leaves by integer value; return collisions. + + A leaf qualifies when (a) its last segment matches `_is_port_segment`, + and (b) its full path contains no `[]` step. Rule (b) excludes + list-element ports — e.g. `genesis.block.witnesses[].port` is the + advertised port of each genesis witness record, not a port the local + process binds, so two witnesses sharing a value is expected. + + Returns sorted list of (value, sorted_paths) for any value bound by more + than one path. Sentinel values in PORT_SENTINELS are excluded. Values + that are not coercible to int (substitutions like `${PORT}` resolved to + strings) are skipped silently — the format/depth gates do not look at + values either, and a non-numeric port is a different class of error. + """ + by_value = {} + for full_path, _depth, is_leaf in keys: + if not is_leaf: + continue + if "[]" in full_path: + continue + seg = full_path.split(".")[-1] + if not _is_port_segment(seg): + continue + try: + raw = tree.get(full_path) + except Exception: + continue + try: + value = int(raw) + except (TypeError, ValueError): + continue + if value in PORT_SENTINELS: + continue + by_value.setdefault(value, []).append(full_path) + return sorted( + (v, sorted(paths)) for v, paths in by_value.items() if len(paths) > 1 + ) + + +def main(argv): + debug = False + args = list(argv[1:]) + if args and args[0] == "--debug": + debug = True + args = args[1:] + if len(args) != 1: + print(f"usage: {argv[0]} [--debug] ", 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 + + try: + tree = ConfigFactory.parse_file(str(path)) + except Exception as e: + print(f"error: failed to parse {path}: {e}", file=sys.stderr) + # Mirror the violation path: emit a single GHA annotation so the + # parse failure surfaces in the PR check summary, not just the log. + print(f"::error file={path},title=reference.conf::failed to parse: {e}") + return 2 + + keys = list(walk(tree, "", 0)) + + if debug: + # Keys are yielded in pyhocon insertion order, which mirrors the + # source file top-to-bottom. Eyeball this against reference.conf to + # confirm coverage; the depth column makes the array `[]` steps + # explicit so MAX_DEPTH math is verifiable by inspection. Trailing + # `/` marks namespace intermediates (have children); bare names are + # leaves — `grep -v '/$'` filters to just leaves. + leaf_count = sum(1 for _, _, lf in keys if lf) + print( + f"DEBUG: {len(keys)} parsed keys " + f"({leaf_count} leaves + {len(keys) - leaf_count} intermediates), " + f"walk order:" + ) + for full_path, depth, is_leaf in keys: + label = full_path if is_leaf else full_path + "/" + print(f" d={depth} {label}") + print() + + format_violations = [] + depth_violations = [] + + # Only check leaves: pyhocon expands a dotted-form declaration like + # `a.b.c = X` into intermediate ConfigTree nodes for `a` and `a.b`. A + # single user-written bad key would otherwise be reported once per + # intermediate AND once as the leaf, multiplying noise. The leaf path + # carries every segment, so checking just leaves covers all segments. + for full_path, depth, is_leaf in keys: + if not is_leaf: + continue + if full_path not in ALLOWLIST: + for seg in full_path.split('.'): + # Strip any number of trailing `[]` markers — nested arrays + # produce segments like `a[][]`. + while seg.endswith('[]'): + seg = seg[:-2] + if seg and not KEY_REGEX.match(seg): + format_violations.append((full_path, seg)) + break + + if depth > MAX_DEPTH: + depth_violations.append((full_path, depth)) + + format_violations.sort() + depth_violations.sort() + + port_collisions = find_port_collisions(tree, keys) + + if format_violations or depth_violations or port_collisions: + lines_out = [] + if format_violations: + lines_out.append( + f"Format violations ({len(format_violations)}) — " + f"each segment must match {KEY_REGEX.pattern}:" + ) + for full_path, seg in format_violations: + lines_out.append(f" format: {full_path} (segment: '{seg}')") + if depth_violations: + if lines_out: + lines_out.append("") + lines_out.append( + f"Depth violations ({len(depth_violations)}) — max depth is {MAX_DEPTH} " + f"(each `[]` array step counts as one level):" + ) + for full_path, depth in depth_violations: + lines_out.append( + f" depth: {full_path} (depth={depth}, max={MAX_DEPTH})" + ) + if port_collisions: + if lines_out: + lines_out.append("") + lines_out.append( + f"Port collisions ({len(port_collisions)}) — distinct service " + f"ports must bind distinct values (sentinels {sorted(PORT_SENTINELS)} exempt):" + ) + for value, paths in port_collisions: + lines_out.append( + f" port: value {value} bound by: {', '.join(paths)}" + ) + print("\n".join(lines_out)) + print() + + # Emit ONE consolidated GHA workflow annotation. All offending entries + # are packed into the annotation body via %0A (GHA's newline escape) + # so the entries are visible in the annotation summary, not just in + # the job log. + entries = [] + for full_path, seg in format_violations: + entries.append(f"format: {full_path} (segment '{seg}')") + for full_path, depth in depth_violations: + entries.append(f"depth: {full_path} (depth={depth}, max={MAX_DEPTH})") + for value, paths in port_collisions: + entries.append(f"port: value {value} bound by {', '.join(paths)}") + body = ( + f"reference.conf has {len(format_violations)} format + " + f"{len(depth_violations)} depth + {len(port_collisions)} port " + f"violation(s):%0A" + "%0A".join(entries) + ) + print(f"::error file={path},title=reference.conf::{body}") + print( + f"FAIL: {len(format_violations)} format + {len(depth_violations)} depth " + f"+ {len(port_collisions)} port violation(s) in {path}", + file=sys.stderr, + ) + return 1 + + print( + f"OK: {path} — {len(keys)} keys, all lowerCamelCase, depth <= {MAX_DEPTH}, " + f"service ports unique" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 00000000000..502fc107f4a --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1 @@ +pyhocon==0.3.63 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 19425209bbc..506a823a4f7 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -103,6 +103,28 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: .github/scripts/requirements.txt + + - name: Install pyhocon + run: pip install --quiet -r .github/scripts/requirements.txt + + - name: Validate reference.conf key names and depth + shell: bash + run: | + python3 .github/scripts/check_reference_conf.py \ + common/src/main/resources/reference.conf + + - name: 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: @@ -110,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/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 1b0e8a6637f..d785951027b 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -120,7 +120,7 @@ public void validate(Object object) throws ContractValidateException { } // Load Config - ConfigLoader.load(context.getStoreFactory()); + ConfigLoader.load(context.getStoreFactory(), isConstantCall); // Warm up registry class OperationRegistry.init(); trx = context.getTrxCap().getInstance(); @@ -267,6 +267,7 @@ public void execute(Object object) throws ContractExeException { result = program.getResult(); result.setException(e); result.rejectInternalTransactions(); + clearExceptionResult(result); result.setRuntimeError(result.getException().getMessage()); logger.info("JVMStackOverFlowException: {}", result.getException().getMessage()); } catch (OutOfTimeException e) { @@ -274,6 +275,7 @@ public void execute(Object object) throws ContractExeException { result = program.getResult(); result.setException(e); result.rejectInternalTransactions(); + clearExceptionResult(result); result.setRuntimeError(result.getException().getMessage()); logger.info("timeout: {}", result.getException().getMessage()); } catch (Throwable e) { @@ -282,6 +284,7 @@ public void execute(Object object) throws ContractExeException { } result = program.getResult(); result.rejectInternalTransactions(); + clearExceptionResult(result); if (Objects.isNull(result.getException())) { logger.error(e.getMessage(), e); result.setException(new RuntimeException("Unknown Throwable")); @@ -310,6 +313,13 @@ public void execute(Object object) throws ContractExeException { } + private void clearExceptionResult(ProgramResult result) { + if (VMConfig.allowTvmOsaka()) { + result.getDeleteAccounts().clear(); + result.getLogInfoList().clear(); + } + } + private void create() throws ContractValidateException { if (!rootRepository.getDynamicPropertiesStore().supportVM()) { diff --git a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java index 53d6caf5691..8c8a69b7dfe 100644 --- a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java @@ -18,6 +18,7 @@ import static org.tron.common.crypto.Hash.sha3omit12; import static org.tron.common.math.Maths.max; import static org.tron.core.config.Parameter.ChainConstant.DELEGATE_COST_BASE_SIZE; +import static org.tron.core.Constant.PER_SIGN_LENGTH; import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; import com.google.common.base.CaseFormat; @@ -183,8 +184,30 @@ public static String makeUpperCamelMethod(String originName) { .replace("_", ""); } + public static Transaction truncateSignatures(Transaction trx) { + Transaction.Builder builder = trx.toBuilder().clearSignature(); + for (ByteString sig : trx.getSignatureList()) { + if (sig.size() > PER_SIGN_LENGTH) { + builder.addSignature(ByteString.copyFrom(sig.substring(0, PER_SIGN_LENGTH).toByteArray())); + } else { + builder.addSignature(sig); + } + } + return builder.build(); + } + public TransactionSignWeight getTransactionSignWeight(Transaction trx) { TransactionSignWeight.Builder tswBuilder = TransactionSignWeight.newBuilder(); + Result.Builder resultBuilder = Result.newBuilder(); + if (trx.getSignatureCount() > chainBaseManager.getDynamicPropertiesStore() + .getTotalSignNum()) { + resultBuilder.setCode(Result.response_code.OTHER_ERROR); + resultBuilder.setMessage("too many signatures"); + tswBuilder.setResult(resultBuilder); + return tswBuilder.build(); + } + + trx = truncateSignatures(trx); TransactionExtention.Builder trxExBuilder = TransactionExtention.newBuilder(); trxExBuilder.setTransaction(trx); trxExBuilder.setTxid(ByteString.copyFrom(Sha256Hash.hash(CommonParameter @@ -193,7 +216,6 @@ public TransactionSignWeight getTransactionSignWeight(Transaction trx) { retBuilder.setResult(true).setCode(response_code.SUCCESS); trxExBuilder.setResult(retBuilder); tswBuilder.setTransaction(trxExBuilder); - Result.Builder resultBuilder = Result.newBuilder(); if (trx.getRawData().getContractCount() == 0) { resultBuilder.setCode(Result.response_code.OTHER_ERROR); diff --git a/actuator/src/main/java/org/tron/core/vm/EnergyCost.java b/actuator/src/main/java/org/tron/core/vm/EnergyCost.java index 3641548b3e5..a2c4b59fdc5 100644 --- a/actuator/src/main/java/org/tron/core/vm/EnergyCost.java +++ b/actuator/src/main/java/org/tron/core/vm/EnergyCost.java @@ -365,6 +365,10 @@ public static long getVoteWitnessCost(Program program) { } public static long getVoteWitnessCost2(Program program) { + if (!VMConfig.allowEnergyAdjustment()) { + return getVoteWitnessCost(program); + } + Stack stack = program.getStack(); long oldMemSize = program.getMemSize(); DataWord amountArrayLength = stack.get(stack.size() - 1).clone(); @@ -388,6 +392,10 @@ public static long getVoteWitnessCost2(Program program) { } public static long getVoteWitnessCost3(Program program) { + if (!VMConfig.allowTvmOsaka()) { + return getVoteWitnessCost2(program); + } + Stack stack = program.getStack(); long oldMemSize = program.getMemSize(); BigInteger amountArrayLength = stack.get(stack.size() - 1).value(); 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 1ac96b9d59d..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,12 +699,19 @@ 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); // check if modulus is zero if (isZero(mod)) { + if (VMConfig.allowTvmOsaka()) { + return Pair.of(true, new byte[modLen]); + } return Pair.of(true, EMPTY_BYTE_ARRAY); } diff --git a/actuator/src/main/java/org/tron/core/vm/VM.java b/actuator/src/main/java/org/tron/core/vm/VM.java index b1d7b027601..585c52b0ce0 100644 --- a/actuator/src/main/java/org/tron/core/vm/VM.java +++ b/actuator/src/main/java/org/tron/core/vm/VM.java @@ -23,8 +23,10 @@ public static void play(Program program, JumpTable jumpTable) { try { long factor = DYNAMIC_ENERGY_FACTOR_DECIMAL; long energyUsage = 0L; + // hoist once per execution: avoids a per-opcode VMConfig.current() thread-local lookup + final boolean allowDynamicEnergy = VMConfig.allowDynamicEnergy(); - if (VMConfig.allowDynamicEnergy()) { + if (allowDynamicEnergy) { factor = program.updateContextContractFactor(); } @@ -47,7 +49,7 @@ public static void play(Program program, JumpTable jumpTable) { String opName = Op.getNameOf(op.getOpcode()); /* spend energy before execution */ long energy = op.getEnergyCost(program); - if (VMConfig.allowDynamicEnergy()) { + if (allowDynamicEnergy) { long actualEnergy = energy; // CALL Ops have special calculation on energy. if (CALL_OPS.contains(op.getOpcode())) { @@ -101,7 +103,7 @@ public static void play(Program program, JumpTable jumpTable) { } } - if (VMConfig.allowDynamicEnergy()) { + if (allowDynamicEnergy) { program.addContextContractUsage(energyUsage); } diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index 881eb861bea..35480935742 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -13,40 +13,48 @@ public class ConfigLoader { //only for unit test public static boolean disable = false; - public static void load(StoreFactory storeFactory) { + // isolate=true: a constant call bound to a non-HEAD (solidity/PBFT) snapshot installs its + // snapshot into a thread-local view instead of the process-wide global, so it cannot pollute + // the flags the block-processing path reads concurrently. + public static void load(StoreFactory storeFactory, boolean isolate) { if (!disable) { DynamicPropertiesStore ds = storeFactory.getChainBaseManager().getDynamicPropertiesStore(); VMConfig.setVmTrace(CommonParameter.getInstance().isVmTrace()); if (ds != null) { VMConfig.initVmHardFork(checkForEnergyLimit(ds)); - VMConfig.initAllowMultiSign(ds.getAllowMultiSign()); - VMConfig.initAllowTvmTransferTrc10(ds.getAllowTvmTransferTrc10()); - VMConfig.initAllowTvmConstantinople(ds.getAllowTvmConstantinople()); - VMConfig.initAllowTvmSolidity059(ds.getAllowTvmSolidity059()); - VMConfig.initAllowShieldedTRC20Transaction(ds.getAllowShieldedTRC20Transaction()); - VMConfig.initAllowTvmIstanbul(ds.getAllowTvmIstanbul()); - VMConfig.initAllowTvmFreeze(ds.getAllowTvmFreeze()); - VMConfig.initAllowTvmVote(ds.getAllowTvmVote()); - VMConfig.initAllowTvmLondon(ds.getAllowTvmLondon()); - VMConfig.initAllowTvmCompatibleEvm(ds.getAllowTvmCompatibleEvm()); - VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx( - ds.getAllowHigherLimitForMaxCpuTimeOfOneTx()); - VMConfig.initAllowTvmFreezeV2(ds.supportUnfreezeDelay() ? 1 : 0); - VMConfig.initAllowOptimizedReturnValueOfChainId( - ds.getAllowOptimizedReturnValueOfChainId()); - VMConfig.initAllowDynamicEnergy(ds.getAllowDynamicEnergy()); - VMConfig.initDynamicEnergyThreshold(ds.getDynamicEnergyThreshold()); - VMConfig.initDynamicEnergyIncreaseFactor(ds.getDynamicEnergyIncreaseFactor()); - VMConfig.initDynamicEnergyMaxFactor(ds.getDynamicEnergyMaxFactor()); - VMConfig.initAllowTvmShangHai(ds.getAllowTvmShangHai()); - VMConfig.initAllowEnergyAdjustment(ds.getAllowEnergyAdjustment()); - VMConfig.initAllowStrictMath(ds.getAllowStrictMath()); - VMConfig.initAllowTvmCancun(ds.getAllowTvmCancun()); - VMConfig.initDisableJavaLangMath(ds.getConsensusLogicOptimization()); - VMConfig.initAllowTvmBlob(ds.getAllowTvmBlob()); - VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); - VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); - VMConfig.initAllowHardenResourceCalculation(ds.getAllowHardenResourceCalculation()); + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowMultiSign = ds.getAllowMultiSign() == 1; + snapshot.allowTvmTransferTrc10 = ds.getAllowTvmTransferTrc10() == 1; + snapshot.allowTvmConstantinople = ds.getAllowTvmConstantinople() == 1; + snapshot.allowTvmSolidity059 = ds.getAllowTvmSolidity059() == 1; + snapshot.allowShieldedTRC20Transaction = ds.getAllowShieldedTRC20Transaction() == 1; + snapshot.allowTvmIstanbul = ds.getAllowTvmIstanbul() == 1; + snapshot.allowTvmFreeze = ds.getAllowTvmFreeze() == 1; + snapshot.allowTvmVote = ds.getAllowTvmVote() == 1; + snapshot.allowTvmLondon = ds.getAllowTvmLondon() == 1; + snapshot.allowTvmCompatibleEvm = ds.getAllowTvmCompatibleEvm() == 1; + snapshot.allowHigherLimitForMaxCpuTimeOfOneTx = + ds.getAllowHigherLimitForMaxCpuTimeOfOneTx() == 1; + snapshot.allowTvmFreezeV2 = ds.supportUnfreezeDelay(); + snapshot.allowOptimizedReturnValueOfChainId = ds.getAllowOptimizedReturnValueOfChainId() == 1; + snapshot.allowDynamicEnergy = ds.getAllowDynamicEnergy() == 1; + snapshot.dynamicEnergyThreshold = ds.getDynamicEnergyThreshold(); + snapshot.dynamicEnergyIncreaseFactor = ds.getDynamicEnergyIncreaseFactor(); + snapshot.dynamicEnergyMaxFactor = ds.getDynamicEnergyMaxFactor(); + snapshot.allowTvmShanghai = ds.getAllowTvmShangHai() == 1; + snapshot.allowEnergyAdjustment = ds.getAllowEnergyAdjustment() == 1; + snapshot.allowStrictMath = ds.getAllowStrictMath() == 1; + snapshot.allowTvmCancun = ds.getAllowTvmCancun() == 1; + snapshot.disableJavaLangMath = ds.getConsensusLogicOptimization() == 1; + snapshot.allowTvmBlob = ds.getAllowTvmBlob() == 1; + snapshot.allowTvmSelfdestructRestriction = ds.getAllowTvmSelfdestructRestriction() == 1; + snapshot.allowTvmOsaka = ds.getAllowTvmOsaka() == 1; + snapshot.allowHardenResourceCalculation = ds.getAllowHardenResourceCalculation() == 1; + if (isolate) { + VMConfig.setLocalSnapshot(snapshot); + } else { + VMConfig.setGlobalSnapshot(snapshot); + } } } } 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 80d972041dc..590859a9fef 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 @@ -1616,11 +1616,19 @@ public ProgramTrace getTrace() { } public void createContract2(DataWord value, DataWord memStart, DataWord memSize, DataWord salt) { + if (VMConfig.allowTvmOsaka()) { + returnDataBuffer = null; // reset return buffer right before the call + } + byte[] senderAddress; - if (VMConfig.allowTvmCompatibleEvm() && getCallDeep() == MAX_DEPTH) { + if ((VMConfig.allowTvmCompatibleEvm() || VMConfig.allowTvmOsaka()) + && getCallDeep() == MAX_DEPTH) { 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/common/utils/Commons.java b/chainbase/src/main/java/org/tron/common/utils/Commons.java index b121e84ecfe..99c20d67f11 100644 --- a/chainbase/src/main/java/org/tron/common/utils/Commons.java +++ b/chainbase/src/main/java/org/tron/common/utils/Commons.java @@ -21,6 +21,8 @@ public class Commons { public static final int ASSET_ISSUE_COUNT_LIMIT_MAX = 1000; + public static final int BASE58_ADDRESS_LENGTH = 34; + public static byte[] decode58Check(String input) { byte[] decodeCheck = Base58.decode(input); if (decodeCheck.length <= 4) { @@ -41,9 +43,16 @@ public static byte[] decode58Check(String input) { return null; } + /** + * Decode a Base58Check address string to its 21-byte form. + */ public static byte[] decodeFromBase58Check(String addressBase58) { if (StringUtils.isEmpty(addressBase58)) { - logger.warn("Warning: Address is empty !!"); + logger.debug("address is empty !!"); + return null; + } + if (addressBase58.length() != BASE58_ADDRESS_LENGTH) { + logger.debug("invalid Base58 address length"); return null; } byte[] address = decode58Check(addressBase58); 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 34b7853d4d1..e6cbd52e595 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -21,6 +21,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.UnknownFieldSet; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -226,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 { @@ -328,6 +329,26 @@ public boolean hasWitnessSignature() { return !getInstance().getBlockHeader().getWitnessSignature().isEmpty(); } + public boolean sanitize() { + boolean blockHasUnknown = !this.block.getUnknownFields().asMap().isEmpty(); + boolean headerHasUnknown = !this.block.getBlockHeader().getUnknownFields().asMap().isEmpty(); + if (!blockHasUnknown && !headerHasUnknown) { + return false; + } + UnknownFieldSet empty = UnknownFieldSet.getDefaultInstance(); + Block.Builder builder = this.block.toBuilder(); + if (blockHasUnknown) { + builder.setUnknownFields(empty); + } + if (headerHasUnknown) { + builder.setBlockHeader(this.block.getBlockHeader().toBuilder() + .setUnknownFields(empty) + .build()); + } + this.block = builder.build(); + return true; + } + @Override public String toString() { StringBuilder toStringBuff = new StringBuilder(); diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java index bb4b70cde1b..b3f560541cf 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -28,6 +28,7 @@ import com.google.protobuf.GeneratedMessageV3; import com.google.protobuf.Internal; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.UnknownFieldSet; import java.io.IOException; import java.security.SignatureException; import java.util.ArrayList; @@ -250,7 +251,7 @@ public static long checkWeight(Permission permission, List sigs, byt long weight = getWeight(permission, address); if (weight == 0) { throw new PermissionException( - ByteArray.toHexString(sig.toByteArray()) + " is signed by " + encode58Check(address) + ByteArray.toHexString(hash) + " is signed by " + encode58Check(address) + " but it is not contained of permission."); } if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_7_1)) { @@ -494,6 +495,16 @@ public static boolean validateSignature(Transaction transaction, return false; } + public boolean sanitize() { + if (this.transaction.getUnknownFields().asMap().isEmpty()) { + return false; + } + this.transaction = this.transaction.toBuilder() + .setUnknownFields(UnknownFieldSet.getDefaultInstance()) + .build(); + return true; + } + public void resetResult() { if (this.getInstance().getRetCount() > 0) { this.transaction = this.getInstance().toBuilder().clearRet().build(); @@ -620,7 +631,7 @@ public void addSign(byte[] privateKey, AccountStore accountStore) .signHash(getTransactionId().getBytes()))); this.transaction = this.transaction.toBuilder().addSignature(sig).build(); } - + private static void checkPermission(int permissionId, Permission permission, Transaction.Contract contract) throws PermissionException { if (permissionId != 0) { if (permission.getType() != PermissionType.Active) { @@ -703,7 +714,7 @@ public boolean validateSignature(AccountStore accountStore, } } isVerified = true; - } + } return true; } 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/codecov.yml b/codecov.yml deleted file mode 100644 index 1b46f3fa8db..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,38 +0,0 @@ -# DEPRECATED: Codecov integration is no longer active. -# Coverage is now handled by JaCoCo + madrapps/jacoco-report in pr-build.yml. -# This file is retained for reference only and can be safely deleted. - -# Post a Codecov comment on pull requests. If don't need comment, use comment: false, else use following -comment: false -#comment: -# # Show coverage diff, flags table, and changed files in the PR comment -# layout: "diff, flags, files" -# # Update existing comment if present, otherwise create a new one -# behavior: default -# # Post a comment even when coverage numbers do not change -# require_changes: false -# # Do not require a base report before posting the comment -# require_base: false -# # Require the PR head commit to have a coverage report -# require_head: true -# # Show both project coverage and patch coverage in the PR comment -# hide_project_coverage: false - -codecov: - # Do not wait for all CI checks to pass before sending notifications - require_ci_to_pass: false - notify: - wait_for_ci: false - -coverage: - status: - project: # PR coverage/project UI - default: - # Compare against the base branch automatically - target: auto - # Allow a small coverage drop tolerance - threshold: 0.02% -# patch: off - patch: # PR coverage/patch UI - default: - target: 60% diff --git a/common/build.gradle b/common/build.gradle index 4309d3dc69a..3fc955f9add 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -21,8 +21,7 @@ dependencies { api 'org.aspectj:aspectjrt:1.9.8' api 'org.aspectj:aspectjweaver:1.9.8' api 'org.aspectj:aspectjtools:1.9.8' - api group: 'com.github.tronprotocol', name: 'libp2p', version: 'release-v2.2.8-SNAPSHOT',{ - //api group: 'io.github.tronprotocol', name: 'libp2p', version: '2.2.7',{ + api group: 'io.github.tronprotocol', name: 'libp2p', version: '2.2.8',{ exclude group: 'io.grpc', module: 'grpc-context' exclude group: 'io.grpc', module: 'grpc-core' exclude group: 'io.grpc', module: 'grpc-netty' diff --git a/common/src/main/java/org/tron/common/logsfilter/trigger/BlockLogTrigger.java b/common/src/main/java/org/tron/common/logsfilter/trigger/BlockLogTrigger.java index b878597045d..8b27bb6ff4c 100644 --- a/common/src/main/java/org/tron/common/logsfilter/trigger/BlockLogTrigger.java +++ b/common/src/main/java/org/tron/common/logsfilter/trigger/BlockLogTrigger.java @@ -27,6 +27,12 @@ public class BlockLogTrigger extends Trigger { @Setter private List transactionList = new ArrayList<>(); + // true when this block is being rolled back due to a chain reorg (fork switch); + // mirrors the Ethereum log "removed" semantics already used by ContractTrigger. + @Getter + @Setter + private boolean removed; + public BlockLogTrigger() { setTriggerName(Trigger.BLOCK_TRIGGER_NAME); } @@ -44,6 +50,8 @@ public String toString() { .append(transactionSize) .append(", latestSolidifiedBlockNumber: ") .append(latestSolidifiedBlockNumber) + .append(", removed: ") + .append(removed) .append(", transactionList: ") .append(transactionList).toString(); } diff --git a/common/src/main/java/org/tron/common/logsfilter/trigger/TransactionLogTrigger.java b/common/src/main/java/org/tron/common/logsfilter/trigger/TransactionLogTrigger.java index a4fb1fddb79..7ccc17f9a1d 100644 --- a/common/src/main/java/org/tron/common/logsfilter/trigger/TransactionLogTrigger.java +++ b/common/src/main/java/org/tron/common/logsfilter/trigger/TransactionLogTrigger.java @@ -97,6 +97,12 @@ public class TransactionLogTrigger extends Trigger { @Setter private Map extMap; + // true when this transaction is being rolled back due to a chain reorg (fork switch); + // mirrors the Ethereum log "removed" semantics already used by ContractTrigger. + @Getter + @Setter + private boolean removed; + public TransactionLogTrigger() { setTriggerName(Trigger.TRANSACTION_TRIGGER_NAME); } diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 3fe1e878ffb..eeb92fdbd60 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -14,7 +14,6 @@ import org.tron.common.logsfilter.FilterQuery; import org.tron.common.setting.RocksDbSettings; import org.tron.core.Constant; -import org.tron.core.config.args.Overlay; import org.tron.core.config.args.SeedNode; import org.tron.core.config.args.Storage; import org.tron.p2p.P2pConfig; @@ -317,9 +316,6 @@ public class CommonParameter { public List backupMembers; @Getter @Setter - public long receiveTcpMinDataLength; // clearParam: 2048 - @Getter - @Setter public boolean isOpenFullTcpDisconnect; @Getter @Setter @@ -411,6 +407,9 @@ public class CommonParameter { @Setter public double rateLimiterDisconnect; // clearParam: 1.0 @Getter + @Setter + public boolean rateLimiterApiNonBlocking = false; + @Getter public RocksDbSettings rocksDBCustomSettings; @Getter public GenesisBlock genesisBlock; @@ -432,8 +431,6 @@ public class CommonParameter { @Getter public Storage storage; @Getter - public Overlay overlay; - @Getter public SeedNode seedNode; @Getter public EventPluginConfig eventPluginConfig; @@ -521,12 +518,7 @@ public class CommonParameter { @Getter @Setter public int pBFTHttpPort; - @Getter - @Setter - public int maxNestingDepth = 100; - @Getter - @Setter - public int maxTokenCount = 100_000; + @Getter @Setter public long pBFTExpireNum; // clearParam: 20 diff --git a/common/src/main/java/org/tron/core/Constant.java b/common/src/main/java/org/tron/core/Constant.java index 1437d319346..5d3f3099c91 100644 --- a/common/src/main/java/org/tron/core/Constant.java +++ b/common/src/main/java/org/tron/core/Constant.java @@ -19,7 +19,8 @@ public class Constant { public static final long MAXIMUM_TIME_UNTIL_EXPIRATION = 24 * 60 * 60 * 1_000L; //one day public static final long TRANSACTION_DEFAULT_EXPIRATION_TIME = 60 * 1_000L; //60 seconds public static final long TRANSACTION_FEE_POOL_PERIOD = 1; //1 blocks - public static final long PER_SIGN_LENGTH = 65L; + public static final int PER_SIGN_LENGTH = 65; + public static final int MAX_PER_SIGN_LENGTH = 68; public static final long MAX_CONTRACT_RESULT_SIZE = 2L; // Smart contract / Energy @@ -63,4 +64,8 @@ public class Constant { // Network public static final String LOCAL_HOST = "127.0.0.1"; + // JSON parsing (DoS protection) + public static final int MAX_NESTING_DEPTH = 20; + public static final int MAX_TOKEN_COUNT = 100_000; + } 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/CommitteeConfig.java b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java index 5cd9de842a0..660fa289e3b 100644 --- a/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigValue; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -35,29 +36,10 @@ public class CommitteeConfig { private long allowProtoFilterNum = 0; private long allowAccountStateRoot = 0; private long changedDelegation = 0; - // NON-STANDARD NAMING: "allowPBFT" and "pBFTExpireNum" in config.conf contain - // consecutive uppercase letters ("PBFT"), which violates JavaBean naming convention. - // ConfigBeanFactory derives config keys from setter names using JavaBean rules: - // setPBFTExpireNum -> property "PBFTExpireNum" (capital P, per JavaBean spec) - // but config.conf uses "pBFTExpireNum" (lowercase p) -> mismatch -> binding fails. - // - // These two fields are excluded from auto-binding and handled manually in fromConfig(). - // TODO: Rename config keys to standard camelCase (allowPbft, pbftExpireNum) when - // PBFT feature is enabled and a breaking config change is acceptable. - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private long allowPBFT = 0; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private long pBFTExpireNum = 20; - - // Only getters are exposed. No public setters — ConfigBeanFactory scans public - // setters via reflection and would derive key "PBFTExpireNum" / "AllowPBFT" - // (JavaBean uppercase rule), which does not match config keys "pBFTExpireNum" - // / "allowPBFT" and would throw. Values are assigned to fields directly in - // fromConfig() below. - public long getAllowPBFT() { return allowPBFT; } - public long getPBFTExpireNum() { return pBFTExpireNum; } + // "allowPBFT" / "pBFTExpireNum" in config.conf use non-standard casing; they are + // remapped to standard camelCase by normalizeNonStandardKeys() before binding. + private long allowPbft = 0; + private long pbftExpireNum = 20; private long allowTvmFreeze = 0; private long allowTvmVote = 0; private long allowTvmLondon = 0; @@ -85,32 +67,30 @@ public class CommitteeConfig { private long dynamicEnergyMaxFactor = 0; // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig - // Defaults come from reference.conf (loaded globally via Configuration.java) - - /** - * Create CommitteeConfig from the "committee" section of the application config. - * - * Note: allowPBFT and pBFTExpireNum have non-standard JavaBean naming (consecutive - * uppercase letters) which causes ConfigBeanFactory key mismatch. These two fields - * are excluded from automatic binding and handled manually after. - */ - private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum"; - private static final String ALLOW_PBFT_KEY = "allowPBFT"; - public static CommitteeConfig fromConfig(Config config) { - Config section = config.getConfig("committee"); - + Config section = normalizeNonStandardKeys(config.getConfig("committee")); CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class); - // Ensure the manually-named fields get the right values from the original keys - cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0; - cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY) - ? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20; - cc.postProcess(); return cc; } + // "allowPBFT" and "pBFTExpireNum" use non-standard casing that JavaBean Introspector + // cannot derive correctly (setPBFTExpireNum -> property "PBFTExpireNum", not "pBFTExpireNum"). + // Remap them to standard camelCase keys so ConfigBeanFactory binds them normally. + // Config is immutable; withValue() returns a new object. + private static Config normalizeNonStandardKeys(Config section) { + if (section.hasPath("allowPBFT")) { + ConfigValue v = section.getValue("allowPBFT"); + section = section.withValue("allowPbft", v); // rename allowPBFT -> allowPbft + } + if (section.hasPath("pBFTExpireNum")) { + ConfigValue v = section.getValue("pBFTExpireNum"); + section = section.withValue("pbftExpireNum", v); // rename pBFTExpireNum -> pbftExpireNum + } + return section; + } + private void postProcess() { // clamp unfreezeDelayDays to 0-365 if (unfreezeDelayDays < 0) { @@ -121,35 +101,61 @@ private void postProcess() { } // clamp allowDelegateOptimization to 0-1 - if (allowDelegateOptimization < 0) { allowDelegateOptimization = 0; } - if (allowDelegateOptimization > 1) { allowDelegateOptimization = 1; } + if (allowDelegateOptimization < 0) { + allowDelegateOptimization = 0; + } + if (allowDelegateOptimization > 1) { + allowDelegateOptimization = 1; + } // clamp allowDynamicEnergy to 0-1 - if (allowDynamicEnergy < 0) { allowDynamicEnergy = 0; } - if (allowDynamicEnergy > 1) { allowDynamicEnergy = 1; } + if (allowDynamicEnergy < 0) { + allowDynamicEnergy = 0; + } + if (allowDynamicEnergy > 1) { + allowDynamicEnergy = 1; + } // clamp dynamicEnergyThreshold to 0-100_000_000_000_000_000 - if (dynamicEnergyThreshold < 0) { dynamicEnergyThreshold = 0; } + if (dynamicEnergyThreshold < 0) { + dynamicEnergyThreshold = 0; + } if (dynamicEnergyThreshold > 100_000_000_000_000_000L) { dynamicEnergyThreshold = 100_000_000_000_000_000L; } // clamp dynamicEnergyIncreaseFactor to 0-10_000 - if (dynamicEnergyIncreaseFactor < 0) { dynamicEnergyIncreaseFactor = 0; } - if (dynamicEnergyIncreaseFactor > 10_000L) { dynamicEnergyIncreaseFactor = 10_000L; } + if (dynamicEnergyIncreaseFactor < 0) { + dynamicEnergyIncreaseFactor = 0; + } + if (dynamicEnergyIncreaseFactor > 10_000L) { + dynamicEnergyIncreaseFactor = 10_000L; + } // clamp dynamicEnergyMaxFactor to 0-100_000 - if (dynamicEnergyMaxFactor < 0) { dynamicEnergyMaxFactor = 0; } - if (dynamicEnergyMaxFactor > 100_000L) { dynamicEnergyMaxFactor = 100_000L; } + if (dynamicEnergyMaxFactor < 0) { + dynamicEnergyMaxFactor = 0; + } + if (dynamicEnergyMaxFactor > 100_000L) { + dynamicEnergyMaxFactor = 100_000L; + } // clamp allowNewReward to 0-1 (must run BEFORE the cross-field check below, // which depends on allowNewReward != 1) - if (allowNewReward < 0) { allowNewReward = 0; } - if (allowNewReward > 1) { allowNewReward = 1; } + if (allowNewReward < 0) { + allowNewReward = 0; + } + if (allowNewReward > 1) { + allowNewReward = 1; + } // clamp memoFee to 0-1_000_000_000 - if (memoFee < 0) { memoFee = 0; } - if (memoFee > 1_000_000_000L) { memoFee = 1_000_000_000L; } + if (memoFee < 0) { + memoFee = 0; + } + if (memoFee > 1_000_000_000L) { + memoFee = 1_000_000_000L; + } // cross-field: allowOldRewardOpt requires at least one reward/vote flag if (allowOldRewardOpt == 1 && allowNewRewardAlgorithm != 1 diff --git a/common/src/main/java/org/tron/core/config/args/EventConfig.java b/common/src/main/java/org/tron/core/config/args/EventConfig.java index ac1731de2dc..f4378682cc3 100644 --- a/common/src/main/java/org/tron/core/config/args/EventConfig.java +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -25,25 +25,21 @@ public class EventConfig { private String server = ""; private String dbconfig = ""; private boolean contractParse = true; - @Getter(lombok.AccessLevel.NONE) + // "native" is a Java reserved word; config key cannot match field name directly. + // @Setter(NONE) prevents ConfigBeanFactory from requiring a "nativeQueue" key. @Setter(lombok.AccessLevel.NONE) private NativeConfig nativeQueue = new NativeConfig(); - public NativeConfig getNativeQueue() { return nativeQueue; } - // Topics list has optional fields (ethCompatible, redundancy, solidified) that - // not all items have. ConfigBeanFactory requires all bean fields to exist in config. - // Excluded from auto-binding, read manually in fromConfig(). - @Getter(lombok.AccessLevel.NONE) + // Topics list items have optional fields; excluded from auto-binding. + // @Setter(NONE) prevents ConfigBeanFactory from requiring a "topics" key. @Setter(lombok.AccessLevel.NONE) private List topics = new ArrayList<>(); - - public List getTopics() { return topics; } private FilterConfig filter = new FilterConfig(); @Getter @Setter public static class NativeConfig { - private boolean useNativeQueue = true; + private boolean useNativeQueue = false; private int bindport = 5555; private int sendqueuelength = 1000; } @@ -70,62 +66,40 @@ public static class FilterConfig { // Defaults come from reference.conf (loaded globally via Configuration.java) + // TopicConfig fields are optional per item; this fallback ensures all keys exist + // for ConfigBeanFactory binding. Values must match TopicConfig field defaults. + private static final Config TOPIC_DEFAULTS = ConfigFactory.parseString( + "triggerName=\"\", enable=false, topic=\"\", " + + "solidified=false, ethCompatible=false, redundancy=false"); + /** * Create EventConfig from the "event.subscribe" section of the application config. * - *

Note: HOCON key "native" is a Java reserved word, so the bean field is named - * "nativeQueue" but config key is "native". We handle this manually after binding. + *

"native" is a Java reserved word, so the field is named "nativeQueue" and the + * sub-section is read directly after binding. "topics" items may omit optional fields; + * TOPIC_DEFAULTS provides fallback values so ConfigBeanFactory can bind each item. */ public static EventConfig fromConfig(Config config) { Config section = config.getConfig("event.subscribe"); - // "native" is a Java reserved word, "topics" has optional fields per item — - // strip both before binding, read manually String nativeKey = "native"; String topicsKey = "topics"; - Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey) - .withoutPath("topicDefaults"); + // remove two keys to construct EventConfig because they cannot be bind automatically, + // we can bind them manually later + Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey); EventConfig ec = ConfigBeanFactory.create(bindable, EventConfig.class); - // manually bind "native" sub-section - Config nativeSection = section.hasPath(nativeKey) - ? section.getConfig(nativeKey) : ConfigFactory.empty(); - ec.nativeQueue = new NativeConfig(); - if (nativeSection.hasPath("useNativeQueue")) { - ec.nativeQueue.useNativeQueue = nativeSection.getBoolean("useNativeQueue"); - } - if (nativeSection.hasPath("bindport")) { - ec.nativeQueue.bindport = nativeSection.getInt("bindport"); - } - if (nativeSection.hasPath("sendqueuelength")) { - ec.nativeQueue.sendqueuelength = nativeSection.getInt("sendqueuelength"); - } + // "native" sub-section: bind via ConfigBeanFactory when present, use defaults otherwise + ec.nativeQueue = section.hasPath(nativeKey) + ? ConfigBeanFactory.create(section.getConfig(nativeKey), NativeConfig.class) + : new NativeConfig(); - // manually bind topics — each item may have optional fields + // topics: withFallback fills optional fields so ConfigBeanFactory can bind each item if (section.hasPath(topicsKey)) { ec.topics = new ArrayList<>(); for (com.typesafe.config.ConfigObject obj : section.getObjectList(topicsKey)) { - Config tc = obj.toConfig(); - TopicConfig topic = new TopicConfig(); - if (tc.hasPath("triggerName")) { - topic.triggerName = tc.getString("triggerName"); - } - if (tc.hasPath("enable")) { - topic.enable = tc.getBoolean("enable"); - } - if (tc.hasPath("topic")) { - topic.topic = tc.getString("topic"); - } - if (tc.hasPath("solidified")) { - topic.solidified = tc.getBoolean("solidified"); - } - if (tc.hasPath("ethCompatible")) { - topic.ethCompatible = tc.getBoolean("ethCompatible"); - } - if (tc.hasPath("redundancy")) { - topic.redundancy = tc.getBoolean("redundancy"); - } - ec.topics.add(topic); + ec.topics.add(ConfigBeanFactory.create( + obj.toConfig().withFallback(TOPIC_DEFAULTS), TopicConfig.class)); } } diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index ea9f26a06a0..2158f56d0ba 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -17,6 +17,7 @@ // ConfigBeanFactory auto-binds all fields including sub-beans, dot-notation keys, // PBFT fields, and list fields. Only legacy key fallbacks and PascalCase shutdown // keys are read manually. +// Always construct via {@link #fromConfig} — direct construction skips postProcess() clamping. @Slf4j @Getter @Setter @@ -29,47 +30,43 @@ public class NodeConfig { private int syncFetchBatchNum = 2000; private int maxPendingBlockSize = 500; private int validateSignThreadNum = 0; // 0 = auto (availableProcessors) - private int maxConnections = 30; + private int maxConnections = 30; // legacy key maxActiveNodes private int minConnections = 8; private int minActiveConnections = 3; - private int maxConnectionsWithSameIp = 2; + private int maxConnectionsWithSameIp = 2; // legacy key maxActiveNodesWithSameIp private int maxHttpConnectNumber = 50; private int minParticipationRate = 0; private boolean openPrintLog = true; private boolean openTransactionSort = false; private int maxTps = 1000; private int maxBlockInvPerSecond = 10; - // Config key "isOpenFullTcpDisconnect" cannot auto-bind — read manually in fromConfig() - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private boolean isOpenFullTcpDisconnect = false; - - public boolean isOpenFullTcpDisconnect() { return isOpenFullTcpDisconnect; } + private boolean openFullTcpDisconnect = false; //rename key // node.discovery.* — HOCON merges into node { discovery { ... } }, auto-bound private DiscoveryConfig discovery = new DiscoveryConfig(); - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private String externalIP = ""; - // node.shutdown.* uses PascalCase keys (BlockTime, BlockHeight, BlockCount) - // that don't match JavaBean naming. Excluded, read manually. - @Getter(lombok.AccessLevel.NONE) + // node.shutdown.* uses PascalCase nested keys (shutdown.BlockTime, etc.). + // These are optional (not in reference.conf), so @Setter(NONE) prevents ConfigBeanFactory + // from requiring the keys; values are read manually in fromConfig(). @Setter(lombok.AccessLevel.NONE) private String shutdownBlockTime = ""; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private long shutdownBlockHeight = -1; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private long shutdownBlockCount = -1; - public boolean isDiscoveryEnable() { return discovery.isEnable(); } - public boolean isDiscoveryPersist() { return discovery.isPersist(); } - public String getDiscoveryExternalIp() { return externalIP; } - public String getShutdownBlockTime() { return shutdownBlockTime; } - public long getShutdownBlockHeight() { return shutdownBlockHeight; } - public long getShutdownBlockCount() { return shutdownBlockCount; } + public boolean isDiscoveryEnable() { + return discovery.isEnable(); + } + + public boolean isDiscoveryPersist() { + return discovery.isPersist(); + } + + public String getDiscoveryExternalIp() { + return discovery.getExternal().getIp(); + } + private int inactiveThreshold = 600; private boolean metricsEnable = false; private int blockProducedTimeOut = 50; @@ -81,7 +78,6 @@ public class NodeConfig { private ValidContractProtoConfig validContractProto = new ValidContractProtoConfig(); private int shieldedTransInPendingMaxCounts = 10; private long blockCacheTimeout = 60; - private long receiveTcpMinDataLength = 2048; private int maxTransactionPendingSize = 2000; private long pendingTransactionTimeout = 60000; private int maxTrxCacheSize = 50_000; @@ -90,14 +86,14 @@ public class NodeConfig { private boolean unsolidifiedBlockCheck = false; private int maxUnsolidifiedBlocks = 54; private String zenTokenId = "000000"; - @Getter(lombok.AccessLevel.NONE) + // allowShieldedTransactionApi is optional (commented out in reference.conf) and has a + // legacy key fallback; @Setter(NONE) prevents ConfigBeanFactory from requiring the key. @Setter(lombok.AccessLevel.NONE) private boolean allowShieldedTransactionApi = false; + + //deprecate key private double activeConnectFactor = 0.1; private double connectFactor = 0.6; - // Legacy alias `maxActiveNodesWithSameIp` has no bean field: we only peek at it via - // section.hasPath() below. Keeping it field-less means reference.conf doesn't have to - // ship a default that would otherwise mask the modern `maxConnectionsWithSameIp` key. // ---- Sub-beans matching config's dot-notation nested structure ---- private ListenConfig listen = new ListenConfig(); @@ -105,11 +101,21 @@ public class NodeConfig { private SolidityConfig solidity = new SolidityConfig(); // Convenience getters for backward compatibility with applyNodeConfig - public int getListenPort() { return listen.getPort(); } - public int getFetchBlockTimeout() { return fetchBlock.getTimeout(); } - public int getSolidityThreads() { return solidity.getThreads(); } - public int getValidContractProtoThreads() { return validContractProto.getThreads(); } - public boolean isAllowShieldedTransactionApi() { return allowShieldedTransactionApi; } + public int getListenPort() { + return listen.getPort(); + } + + public int getFetchBlockTimeout() { + return fetchBlock.getTimeout(); + } + + public int getSolidityThreads() { + return solidity.getThreads(); + } + + public int getValidContractProtoThreads() { + return validContractProto.getThreads(); + } // ---- List fields (manually read) ---- private List active = new ArrayList<>(); @@ -136,112 +142,83 @@ public class NodeConfig { @Getter @Setter public static class DiscoveryConfig { + private boolean enable = false; private boolean persist = false; + private ExternalConfig external = new ExternalConfig(); + + @Getter + @Setter + public static class ExternalConfig { + + private String ip = ""; + } } @Getter @Setter public static class ListenConfig { + private int port = 18888; } @Getter @Setter public static class FetchBlockConfig { + private int timeout = 500; } @Getter @Setter public static class SolidityConfig { + private int threads = 0; // 0 = auto (availableProcessors) } @Getter @Setter public static class ValidContractProtoConfig { + private int threads = 0; // 0 = auto (availableProcessors) } @Getter @Setter public static class P2pConfig { + private int version = 11111; } @Getter @Setter public static class HttpConfig { + private boolean fullNodeEnable = true; private int fullNodePort = 8090; private boolean solidityEnable = true; private int solidityPort = 8091; private long maxMessageSize = 4194304; - private int maxNestingDepth = 100; - private int maxTokenCount = 100_000; - // PBFT fields — handled manually (same naming issue as CommitteeConfig) - // Default must match CommonParameter.pBFTHttpEnable = true - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private boolean pBFTEnable = true; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int pBFTPort = 8092; - - public boolean isPBFTEnable() { - return pBFTEnable; - } - - public void setPBFTEnable(boolean v) { - this.pBFTEnable = v; - } - - public int getPBFTPort() { - return pBFTPort; - } - - public void setPBFTPort(int v) { - this.pBFTPort = v; - } } @Getter @Setter public static class RpcConfig { + private boolean enable = true; private int port = 50051; private boolean solidityEnable = true; private int solidityPort = 50061; - // PBFT fields — handled manually - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private boolean pBFTEnable = true; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int pBFTPort = 50071; - public boolean isPBFTEnable() { - return pBFTEnable; - } - - public void setPBFTEnable(boolean v) { - this.pBFTEnable = v; - } - - public int getPBFTPort() { - return pBFTPort; - } - - public void setPBFTPort(int v) { - this.pBFTPort = v; - } - private int thread = 0; - private int maxConcurrentCallsPerConnection = 2147483647; + private int maxConcurrentCallsPerConnection = 0; private int flowControlWindow = 1048576; - private long maxConnectionIdleInMillis = Long.MAX_VALUE; - private long maxConnectionAgeInMillis = Long.MAX_VALUE; + private long maxConnectionIdleInMillis = 0; + private long maxConnectionAgeInMillis = 0; private int maxMessageSize = 4194304; private int maxHeaderListSize = 8192; private int maxRstStream = 0; @@ -254,34 +231,14 @@ public void setPBFTPort(int v) { @Getter @Setter public static class JsonRpcConfig { + private boolean httpFullNodeEnable = false; private int httpFullNodePort = 8545; private boolean httpSolidityEnable = false; private int httpSolidityPort = 8555; - // PBFT fields — handled manually - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private boolean httpPBFTEnable = false; - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) private int httpPBFTPort = 8565; - public boolean isHttpPBFTEnable() { - return httpPBFTEnable; - } - - public void setHttpPBFTEnable(boolean v) { - this.httpPBFTEnable = v; - } - - public int getHttpPBFTPort() { - return httpPBFTPort; - } - - public void setHttpPBFTPort(int v) { - this.httpPBFTPort = v; - } - private int maxBlockRange = 5000; private int maxSubTopics = 1000; private int maxBlockFilterNum = 50000; @@ -295,6 +252,7 @@ public void setHttpPBFTPort(int v) { @Getter @Setter public static class NodeBackupConfig { + private int priority = 0; private int port = 10001; private int keepAliveInterval = 3000; @@ -304,6 +262,7 @@ public static class NodeBackupConfig { @Getter @Setter public static class DynamicConfigSection { + private boolean enable = false; private long checkInterval = 600; } @@ -311,14 +270,15 @@ public static class DynamicConfigSection { @Getter @Setter public static class DnsConfig { + private List treeUrls = new ArrayList<>(); private boolean publish = false; private String dnsDomain = ""; private String dnsPrivate = ""; private List knownUrls = new ArrayList<>(); private List staticNodes = new ArrayList<>(); - private int maxMergeSize = 0; - private double changeThreshold = 0.0; + private int maxMergeSize = 5; + private double changeThreshold = 0.1; private String serverType = ""; private String accessKeyId = ""; private String accessKeySecret = ""; @@ -327,62 +287,44 @@ public static class DnsConfig { private String awsHostZoneId = ""; } - // Defaults come from reference.conf (loaded globally via Configuration.java) - - // =========================================================================== - // Factory method - // =========================================================================== - /** * Create NodeConfig from the "node" section of the application config. * - *

Dot-notation keys (listen.port, fetchBlock.timeout, - * solidity.threads) become nested HOCON objects and cannot be auto-bound to flat - * Java fields. They are read manually after ConfigBeanFactory binding. - * - *

PBFT-named fields in http, rpc, and jsonrpc sub-beans have the same JavaBean - * naming issue as CommitteeConfig and are patched manually. - * *

List fields (active, passive, fastForward, disabledApi) are read manually * since ConfigBeanFactory expects typed bean lists, not string lists. */ public static NodeConfig fromConfig(Config config) { - // Normalize human-readable size values (e.g. "4m") to numeric bytes so - // ConfigBeanFactory's primitive int/long binding succeeds; same step - // enforces non-negative and <= Integer.MAX_VALUE before bean creation - // so failures point at the user-facing config path. - Config section = normalizeMaxMessageSizes(config).getConfig("node"); + Config section = normalizeNonStandardKeys(config.getConfig("node")); // Auto-bind all fields and sub-beans. ConfigBeanFactory fails fast with a - // descriptive path on any `= null` value — external configs that use the - // HOCON null keyword should fix their config rather than rely on silent coercion. + // descriptive path on any `= null` value NodeConfig nc = ConfigBeanFactory.create(section, NodeConfig.class); - // isOpenFullTcpDisconnect: boolean "is" prefix breaks JavaBean pairing - nc.isOpenFullTcpDisconnect = getBool(section, "isOpenFullTcpDisconnect", false); - // --- Legacy key fallbacks (backward compatibility) --- // node.maxActiveNodes (old) -> maxConnections (new) if (section.hasPath("maxActiveNodes")) { + logger.warn("Configuring [node.maxActiveNodes] is deprecated and will be removed in a future " + + "release. Please use [node.maxConnections] instead."); nc.maxConnections = section.getInt("maxActiveNodes"); if (section.hasPath("connectFactor")) { + logger.warn("Configuring [node.connectFactor] is deprecated and will be removed in a future " + + "release."); nc.minConnections = (int) (nc.maxConnections * section.getDouble("connectFactor")); } if (section.hasPath("activeConnectFactor")) { + logger.warn("Configuring [node.activeConnectFactor] is deprecated and will be removed in a " + + "future release."); nc.minActiveConnections = (int) (nc.maxConnections * section.getDouble("activeConnectFactor")); } } if (section.hasPath("maxActiveNodesWithSameIp")) { + logger.warn("Configuring [node.maxActiveNodesWithSameIp] is deprecated and will be removed " + + "in a future release. Please use [node.maxConnectionsWithSameIp] instead."); nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); } - nc.externalIP = getString(section, "discovery.external.ip", ""); - if ("null".equalsIgnoreCase(nc.externalIP)) { - nc.externalIP = ""; - } - - // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. + // Legacy key fallback: node.allowShieldedTransactionApi wins fullNodeAllowShieldedTransaction if (section.hasPath("allowShieldedTransactionApi")) { nc.allowShieldedTransactionApi = section.getBoolean("allowShieldedTransactionApi"); @@ -394,14 +336,13 @@ public static NodeConfig fromConfig(Config config) { + "Please use [node.allowShieldedTransactionApi] instead."); } - // node.shutdown.* — PascalCase keys (BlockTime, BlockHeight), cannot auto-bind - nc.shutdownBlockTime = config.hasPath("node.shutdown.BlockTime") - ? config.getString("node.shutdown.BlockTime") : ""; - nc.shutdownBlockHeight = config.hasPath("node.shutdown.BlockHeight") - ? config.getLong("node.shutdown.BlockHeight") : -1; - nc.shutdownBlockCount = config.hasPath("node.shutdown.BlockCount") - ? config.getLong("node.shutdown.BlockCount") : -1; - + // node.shutdown.* — optional PascalCase nested keys, not in reference.conf by default + nc.shutdownBlockTime = section.hasPath("shutdown.BlockTime") + ? section.getString("shutdown.BlockTime") : ""; + nc.shutdownBlockHeight = section.hasPath("shutdown.BlockHeight") + ? section.getLong("shutdown.BlockHeight") : -1; + nc.shutdownBlockCount = section.hasPath("shutdown.BlockCount") + ? section.getLong("shutdown.BlockCount") : -1; nc.postProcess(); return nc; @@ -417,6 +358,16 @@ private void postProcess() { rpc.thread = (Runtime.getRuntime().availableProcessors() + 1) / 2; } + if (rpc.maxConcurrentCallsPerConnection == 0) { + rpc.maxConcurrentCallsPerConnection = Integer.MAX_VALUE; + } + if (rpc.maxConnectionIdleInMillis == 0) { + rpc.maxConnectionIdleInMillis = Long.MAX_VALUE; + } + if (rpc.maxConnectionAgeInMillis == 0) { + rpc.maxConnectionAgeInMillis = Long.MAX_VALUE; + } + // validateSignThreadNum: 0 = auto-detect if (validateSignThreadNum == 0) { validateSignThreadNum = Runtime.getRuntime().availableProcessors(); @@ -440,6 +391,14 @@ private void postProcess() { syncFetchBatchNum = 100; } + // fetchBlock.timeout : clamp to [100, 1000] + if (fetchBlock.timeout > 1000) { + fetchBlock.timeout = 1000; + } + if (fetchBlock.timeout < 100) { + fetchBlock.timeout = 100; + } + // maxPendingBlockSize: clamp to [50, 2000] if (maxPendingBlockSize > 2000) { maxPendingBlockSize = 2000; @@ -491,6 +450,20 @@ private void postProcess() { if (maxTrxCacheSize < 2000) { maxTrxCacheSize = 2000; } + + // maxMessageSize: reject negative values + if (rpc.maxMessageSize < 0) { + throw new TronError("node.rpc.maxMessageSize must be non-negative, got: " + + rpc.maxMessageSize, PARAMETER_INIT); + } + if (http.maxMessageSize < 0) { + throw new TronError("node.http.maxMessageSize must be non-negative, got: " + + http.maxMessageSize, PARAMETER_INIT); + } + if (jsonrpc.maxMessageSize < 0) { + throw new TronError("node.jsonrpc.maxMessageSize must be non-negative, got: " + + jsonrpc.maxMessageSize, PARAMETER_INIT); + } } // =========================================================================== @@ -513,34 +486,22 @@ private static String getString(Config config, String path, String defaultValue) return config.hasPath(path) ? config.getString(path) : defaultValue; } - // Pre-normalize size paths so ConfigBeanFactory's primitive int/long binding succeeds - // for human-readable values like "4m" / "128MB". For each maxMessageSize key, parse - // via getMemorySize, validate non-negative and <= Integer.MAX_VALUE, and write the - // numeric byte value back into the Config tree. Validation errors propagate before - // bean creation so the failure points at the user-facing config path. - private static Config normalizeMaxMessageSizes(Config config) { - String[] paths = { - "node.rpc.maxMessageSize", - "node.http.maxMessageSize", - "node.jsonrpc.maxMessageSize" - }; - Config result = config; - for (String path : paths) { - if (config.hasPath(path)) { - long bytes = parseMaxMessageSize(config, path); - result = result.withValue(path, ConfigValueFactory.fromAnyRef(bytes)); - } + /** + * "isOpenFullTcpDisconnect" config key has an "is" prefix that the JavaBean Introspector + * strips from boolean getter names, so the derived property is "openFullTcpDisconnect". + * "discovery.external.ip" may be HOCON null or the string "null"; both normalize to "". + */ + private static Config normalizeNonStandardKeys(Config section) { + if (section.hasPath("isOpenFullTcpDisconnect")) { + section = section.withValue("openFullTcpDisconnect", + section.getValue("isOpenFullTcpDisconnect")); } - return result; - } - - private static long parseMaxMessageSize(Config config, String key) { - long value = config.getMemorySize(key).toBytes(); - if (value < 0 || value > Integer.MAX_VALUE) { - throw new TronError(key + " must be non-negative and <= " - + Integer.MAX_VALUE + ", got: " + value, PARAMETER_INIT); + String externalIpPath = "discovery.external.ip"; + if (section.getIsNull(externalIpPath) + || "null".equalsIgnoreCase(section.getString(externalIpPath))) { + section = section.withValue(externalIpPath, ConfigValueFactory.fromAnyRef("")); } - return value; + return section; } } diff --git a/common/src/main/java/org/tron/core/config/args/Overlay.java b/common/src/main/java/org/tron/core/config/args/Overlay.java deleted file mode 100644 index bdaa40724c7..00000000000 --- a/common/src/main/java/org/tron/core/config/args/Overlay.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.tron.core.config.args; - -import lombok.Getter; -import org.apache.commons.lang3.Range; - -public class Overlay { - - @Getter - private int port; - - /** - * Monitor port number. - */ - public void setPort(final int port) { - Range range = Range.between(0, 65535); - if (!range.contains(port)) { - throw new IllegalArgumentException("Port(" + port + ") must in [0, 65535]"); - } - - this.port = port; - } -} diff --git a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java index eed5ef1898b..5eab6f6d92d 100644 --- a/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -21,6 +21,7 @@ public class RateLimiterConfig { private P2pRateLimitConfig p2p = new P2pRateLimitConfig(); private List http = new ArrayList<>(); private List rpc = new ArrayList<>(); + private boolean apiNonBlocking = false; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 782a0ef07c8..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; @@ -70,17 +69,6 @@ public class Storage { @Setter private int maxFlushCount; - /** - * Index storage directory: /path/to/{indexDirectory} - */ - @Getter - @Setter - private String indexDirectory; - - @Getter - @Setter - private String indexSwitch; - @Getter @Setter private boolean contractParseSwitch; @@ -181,30 +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 of Storage object from Config via StorageConfig bean. - */ /** * Set propertyMap from StorageConfig bean list. No Config parameter needed. */ @@ -262,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 3d7046ebae2..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 @@ -21,7 +21,6 @@ public class StorageConfig { private DbConfig db = new DbConfig(); - private IndexConfig index = new IndexConfig(); private TransHistoryConfig transHistory = new TransHistoryConfig(); private boolean needToUpdateAsset = true; private DbSettingsConfig dbSettings = new DbSettingsConfig(); @@ -29,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. @@ -39,61 +40,33 @@ public class StorageConfig { // Raw storage config sub-tree, kept for setCacheStrategies/setDbRoots which // have dynamic keys that ConfigBeanFactory cannot bind. - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private Config rawStorageConfig; - public Config getRawStorageConfig() { - return rawStorageConfig; - } - // LevelDB per-database option overrides (default, defaultM, defaultL). - // Excluded from auto-binding: optional partial overrides that ConfigBeanFactory cannot handle. - @Getter(lombok.AccessLevel.NONE) + // @Setter(NONE): optional keys commented out in reference.conf; ConfigBeanFactory + // would throw if it required them. Values are assigned in fromConfig(). @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultDbOption; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultMDbOption; - @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private DbOptionOverride defaultLDbOption; - public DbOptionOverride getDefaultDbOption() { return defaultDbOption; } - public DbOptionOverride getDefaultMDbOption() { return defaultMDbOption; } - public DbOptionOverride getDefaultLDbOption() { return defaultLDbOption; } - @Getter @Setter public static class DbConfig { + private String engine = "LEVELDB"; private boolean sync = false; private String directory = "database"; } - @Getter - @Setter - public static class IndexConfig { - private String directory = "index"; - // "switch" is a Java keyword, but HOCON key is "index.switch" - // ConfigBeanFactory would look for setSwitch which works fine in Java - @Getter(lombok.AccessLevel.NONE) - @Setter(lombok.AccessLevel.NONE) - private String switchValue = "on"; - - public String getSwitch() { - return switchValue; - } - - public void setSwitch(String v) { - this.switchValue = v; - } - } - @Getter @Setter public static class TransHistoryConfig { - // "switch" is a Java keyword — same handling as IndexConfig + + // "switch" is a reserved Java keyword; ConfigBeanFactory calls setSwitch() which works fine @Getter(lombok.AccessLevel.NONE) @Setter(lombok.AccessLevel.NONE) private String switchValue = "on"; @@ -110,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; @@ -131,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; } } @@ -143,6 +119,7 @@ public static class HistoryConfig { @Getter @Setter public static class CheckpointConfig { + private int version = 1; private boolean sync = true; } @@ -150,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. @@ -166,6 +144,7 @@ void postProcess() { @Getter @Setter public static class TxCacheConfig { + private int estimatedTransactions = 1000; private boolean initOptimization = false; @@ -179,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) @@ -201,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"); @@ -218,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 { @@ -289,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/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index 00ba85aa6cc..3ff1136f33e 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -2,24 +2,18 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; -import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** * VM configuration bean. Field names match config.conf keys under the "vm" section. - * Most fields are bound automatically via ConfigBeanFactory; opt-in fields that - * must stay absent from reference.conf are bound manually after hasPath checks. */ @Slf4j @Getter @Setter public class VmConfig { - private static final String CONSTANT_CALL_TIMEOUT_MS_KEY = "constantCallTimeoutMs"; - static final long MAX_CONSTANT_CALL_TIMEOUT_MS = Long.MAX_VALUE / 1_000L; - private boolean supportConstant = false; private long maxEnergyLimitForConstant = 100_000_000L; private int lruCacheSize = 500; @@ -32,10 +26,6 @@ public class VmConfig { private boolean saveInternalTx = false; private boolean saveFeaturedInternalTx = false; private boolean saveCancelAllUnfreezeV2Details = false; - // Excluded from ConfigBeanFactory binding (no setter): the property is - // intentionally absent from reference.conf so {@code Config#hasPath} alone - // signals operator opt-in. Bound manually in {@link #fromConfig}. - @Setter(AccessLevel.NONE) private long constantCallTimeoutMs = 0L; /** @@ -46,11 +36,11 @@ public class VmConfig { public static VmConfig fromConfig(Config config) { Config vmSection = config.getConfig("vm"); VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); - vmConfig.postProcess(vmSection); + vmConfig.postProcess(); return vmConfig; } - private void postProcess(Config vmSection) { + private void postProcess() { // clamp maxEnergyLimitForConstant if (maxEnergyLimitForConstant < 3_000_000L) { maxEnergyLimitForConstant = 3_000_000L; @@ -71,22 +61,9 @@ private void postProcess(Config vmSection) { + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); } - // constantCallTimeoutMs is excluded from ConfigBeanFactory binding (no - // setter) and intentionally absent from reference.conf, so hasPath alone - // tells us whether the operator opted in. Only positive values that can be - // safely converted to microseconds are valid. - if (vmSection.hasPath(CONSTANT_CALL_TIMEOUT_MS_KEY)) { - long value = vmSection.getLong(CONSTANT_CALL_TIMEOUT_MS_KEY); - if (value <= 0L) { - throw new IllegalArgumentException( - "vm.constantCallTimeoutMs must be > 0 when configured, got " + value); - } - if (value > MAX_CONSTANT_CALL_TIMEOUT_MS) { - throw new IllegalArgumentException( - "vm.constantCallTimeoutMs must be <= " + MAX_CONSTANT_CALL_TIMEOUT_MS - + " to fit VM deadline conversion, got " + value); - } - constantCallTimeoutMs = value; + if (constantCallTimeoutMs < 0 || constantCallTimeoutMs > Long.MAX_VALUE / 1000) { + throw new IllegalArgumentException("vm.constantCallTimeoutMs must be >= 0 and <= " + + Long.MAX_VALUE / 1000 + " to fit VM deadline conversion, got " + constantCallTimeoutMs); } } } diff --git a/common/src/main/java/org/tron/core/exception/P2pException.java b/common/src/main/java/org/tron/core/exception/P2pException.java index eae830627c2..5c2f21778a3 100644 --- a/common/src/main/java/org/tron/core/exception/P2pException.java +++ b/common/src/main/java/org/tron/core/exception/P2pException.java @@ -50,8 +50,8 @@ public enum TypeEnum { TRX_EXE_FAILED(12, "trx exe failed"), DB_ITEM_NOT_FOUND(13, "DB item not found"), PROTOBUF_ERROR(14, "protobuf inconsistent"), - BLOCK_SIGN_ERROR(15, "block sign error"), - BLOCK_MERKLE_ERROR(16, "block merkle error"), + BLOCK_SIGN_INVALID(15, "block sign invalid"), + BLOCK_MERKLE_INVALID(16, "block merkle invalid"), RATE_LIMIT_EXCEEDED(17, "rate limit exceeded"), DEFAULT(100, "default exception"); diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 94c1e50284e..304ced33698 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -13,57 +13,74 @@ public class VMConfig { @Setter private static boolean vmTrace = false; - private static boolean ALLOW_TVM_TRANSFER_TRC10 = false; - - private static boolean ALLOW_TVM_CONSTANTINOPLE = false; - - private static boolean ALLOW_MULTI_SIGN = false; - - private static boolean ALLOW_TVM_SOLIDITY_059 = false; - - private static boolean ALLOW_SHIELDED_TRC20_TRANSACTION = false; - - private static boolean ALLOW_TVM_ISTANBUL = false; - - private static boolean ALLOW_TVM_FREEZE = false; - - private static boolean ALLOW_TVM_VOTE = false; - - private static boolean ALLOW_TVM_LONDON = false; - - private static boolean ALLOW_TVM_COMPATIBLE_EVM = false; - - private static boolean ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX = false; - - private static boolean ALLOW_TVM_FREEZE_V2 = false; - - private static boolean ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID = false; - - private static boolean ALLOW_DYNAMIC_ENERGY = false; - - private static long DYNAMIC_ENERGY_THRESHOLD = 0L; - - private static long DYNAMIC_ENERGY_INCREASE_FACTOR = 0L; - - private static long DYNAMIC_ENERGY_MAX_FACTOR = 0L; - - private static boolean ALLOW_TVM_SHANGHAI = false; - - private static boolean ALLOW_ENERGY_ADJUSTMENT = false; - - private static boolean ALLOW_STRICT_MATH = false; - - private static boolean ALLOW_TVM_CANCUN = false; - - private static Boolean DISABLE_JAVA_LANG_MATH = false; - - private static boolean ALLOW_TVM_BLOB = false; - - private static boolean ALLOW_TVM_SELFDESTRUCT_RESTRICTION = false; - - private static boolean ALLOW_TVM_OSAKA = false; - - private static boolean ALLOW_HARDEN_RESOURCE_CALCULATION = false; + /** + * Snapshot of all chain/store-derived VM config flags. The block-processing (HEAD) path + * installs it as the process-wide {@link #globalSnapshot}; a constant call executing against a + * non-HEAD (solidity/PBFT) snapshot installs its own view into {@link #localSnapshot} so it never + * overwrites the flags the consensus path relies on. + */ + public static class Snapshot { + public boolean allowTvmTransferTrc10; + public boolean allowTvmConstantinople; + public boolean allowMultiSign; + public boolean allowTvmSolidity059; + public boolean allowShieldedTRC20Transaction; + public boolean allowTvmIstanbul; + public boolean allowTvmFreeze; + public boolean allowTvmVote; + public boolean allowTvmLondon; + public boolean allowTvmCompatibleEvm; + public boolean allowHigherLimitForMaxCpuTimeOfOneTx; + public boolean allowTvmFreezeV2; + public boolean allowOptimizedReturnValueOfChainId; + public boolean allowDynamicEnergy; + public long dynamicEnergyThreshold; + public long dynamicEnergyIncreaseFactor; + public long dynamicEnergyMaxFactor; + public boolean allowTvmShanghai; + public boolean allowEnergyAdjustment; + public boolean allowStrictMath; + public boolean allowTvmCancun; + public boolean disableJavaLangMath; + public boolean allowTvmBlob; + public boolean allowTvmSelfdestructRestriction; + public boolean allowTvmOsaka; + public boolean allowHardenResourceCalculation; + } + + // HEAD / block-processing config, written by the consensus path; read by everyone with no + // thread-local override. volatile so a wholesale install is safely published across threads. + private static volatile Snapshot globalSnapshot = new Snapshot(); + + // Per-thread override used only by constant calls bound to a non-HEAD (solidity/PBFT) snapshot. + private static final ThreadLocal localSnapshot = new ThreadLocal<>(); + + private static Snapshot current() { + Snapshot local = localSnapshot.get(); + return local != null ? local : globalSnapshot; + } + + /** + * Install the process-wide (HEAD / block-processing) config and drop any thread-local view. + */ + public static void setGlobalSnapshot(Snapshot snapshot) { + globalSnapshot = snapshot; + localSnapshot.remove(); + } + + /** + * Install a thread-local config view for a constant call executing against a non-HEAD snapshot. + */ + public static void setLocalSnapshot(Snapshot snapshot) { + localSnapshot.set(snapshot); + } + + /** + * Drop the thread-local config view so this thread falls back to the global config. + */ + public static void clearLocalSnapshot() { + localSnapshot.remove(); + } private VMConfig() { } @@ -80,108 +97,111 @@ public static void initVmHardFork(boolean pass) { CommonParameter.ENERGY_LIMIT_HARD_FORK = pass; } + // The init* setters below mutate the global (HEAD) config in place. They are kept for tests and + // legacy callers; production config loading goes through ConfigLoader -> setGlobalSnapshot, which + // publishes a fresh Snapshot wholesale via the volatile field. public static void initAllowMultiSign(long allow) { - ALLOW_MULTI_SIGN = allow == 1; + globalSnapshot.allowMultiSign = allow == 1; } public static void initAllowTvmTransferTrc10(long allow) { - ALLOW_TVM_TRANSFER_TRC10 = allow == 1; + globalSnapshot.allowTvmTransferTrc10 = allow == 1; } public static void initAllowTvmConstantinople(long allow) { - ALLOW_TVM_CONSTANTINOPLE = allow == 1; + globalSnapshot.allowTvmConstantinople = allow == 1; } public static void initAllowTvmSolidity059(long allow) { - ALLOW_TVM_SOLIDITY_059 = allow == 1; + globalSnapshot.allowTvmSolidity059 = allow == 1; } public static void initAllowShieldedTRC20Transaction(long allow) { - ALLOW_SHIELDED_TRC20_TRANSACTION = allow == 1; + globalSnapshot.allowShieldedTRC20Transaction = allow == 1; } public static void initAllowTvmIstanbul(long allow) { - ALLOW_TVM_ISTANBUL = allow == 1; + globalSnapshot.allowTvmIstanbul = allow == 1; } public static void initAllowTvmFreeze(long allow) { - ALLOW_TVM_FREEZE = allow == 1; + globalSnapshot.allowTvmFreeze = allow == 1; } public static void initAllowTvmVote(long allow) { - ALLOW_TVM_VOTE = allow == 1; + globalSnapshot.allowTvmVote = allow == 1; } public static void initAllowTvmLondon(long allow) { - ALLOW_TVM_LONDON = allow == 1; + globalSnapshot.allowTvmLondon = allow == 1; } public static void initAllowTvmCompatibleEvm(long allow) { - ALLOW_TVM_COMPATIBLE_EVM = allow == 1; + globalSnapshot.allowTvmCompatibleEvm = allow == 1; } public static void initAllowHigherLimitForMaxCpuTimeOfOneTx(long allow) { - ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX = allow == 1; + globalSnapshot.allowHigherLimitForMaxCpuTimeOfOneTx = allow == 1; } public static void initAllowTvmFreezeV2(long allow) { - ALLOW_TVM_FREEZE_V2 = allow == 1; + globalSnapshot.allowTvmFreezeV2 = allow == 1; } public static void initAllowOptimizedReturnValueOfChainId(long allow) { - ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID = allow == 1; + globalSnapshot.allowOptimizedReturnValueOfChainId = allow == 1; } public static void initAllowDynamicEnergy(long allow) { - ALLOW_DYNAMIC_ENERGY = allow == 1; + globalSnapshot.allowDynamicEnergy = allow == 1; } public static void initDynamicEnergyThreshold(long threshold) { - DYNAMIC_ENERGY_THRESHOLD = threshold; + globalSnapshot.dynamicEnergyThreshold = threshold; } public static void initDynamicEnergyIncreaseFactor(long increaseFactor) { - DYNAMIC_ENERGY_INCREASE_FACTOR = increaseFactor; + globalSnapshot.dynamicEnergyIncreaseFactor = increaseFactor; } public static void initDynamicEnergyMaxFactor(long maxFactor) { - DYNAMIC_ENERGY_MAX_FACTOR = maxFactor; + globalSnapshot.dynamicEnergyMaxFactor = maxFactor; } public static void initAllowTvmShangHai(long allow) { - ALLOW_TVM_SHANGHAI = allow == 1; + globalSnapshot.allowTvmShanghai = allow == 1; } public static void initAllowEnergyAdjustment(long allow) { - ALLOW_ENERGY_ADJUSTMENT = allow == 1; + globalSnapshot.allowEnergyAdjustment = allow == 1; } public static void initAllowStrictMath(long allow) { - ALLOW_STRICT_MATH = allow == 1; + globalSnapshot.allowStrictMath = allow == 1; } public static void initAllowTvmCancun(long allow) { - ALLOW_TVM_CANCUN = allow == 1; + globalSnapshot.allowTvmCancun = allow == 1; } public static void initDisableJavaLangMath(long allow) { - DISABLE_JAVA_LANG_MATH = allow == 1; + globalSnapshot.disableJavaLangMath = allow == 1; } public static void initAllowTvmBlob(long allow) { - ALLOW_TVM_BLOB = allow == 1; + globalSnapshot.allowTvmBlob = allow == 1; } public static void initAllowTvmSelfdestructRestriction(long allow) { - ALLOW_TVM_SELFDESTRUCT_RESTRICTION = allow == 1; + globalSnapshot.allowTvmSelfdestructRestriction = allow == 1; } public static void initAllowTvmOsaka(long allow) { - ALLOW_TVM_OSAKA = allow == 1; + globalSnapshot.allowTvmOsaka = allow == 1; } public static void initAllowHardenResourceCalculation(long allow) { - ALLOW_HARDEN_RESOURCE_CALCULATION = allow == 1; + globalSnapshot.allowHardenResourceCalculation = allow == 1; } public static boolean getEnergyLimitHardFork() { @@ -189,106 +209,106 @@ public static boolean getEnergyLimitHardFork() { } public static boolean allowTvmTransferTrc10() { - return ALLOW_TVM_TRANSFER_TRC10; + return current().allowTvmTransferTrc10; } public static boolean allowTvmConstantinople() { - return ALLOW_TVM_CONSTANTINOPLE; + return current().allowTvmConstantinople; } public static boolean allowMultiSign() { - return ALLOW_MULTI_SIGN; + return current().allowMultiSign; } public static boolean allowTvmSolidity059() { - return ALLOW_TVM_SOLIDITY_059; + return current().allowTvmSolidity059; } public static boolean allowShieldedTRC20Transaction() { - return ALLOW_SHIELDED_TRC20_TRANSACTION; + return current().allowShieldedTRC20Transaction; } public static boolean allowTvmIstanbul() { - return ALLOW_TVM_ISTANBUL; + return current().allowTvmIstanbul; } public static boolean allowTvmFreeze() { - return ALLOW_TVM_FREEZE; + return current().allowTvmFreeze; } public static boolean allowTvmVote() { - return ALLOW_TVM_VOTE; + return current().allowTvmVote; } public static boolean allowTvmLondon() { - return ALLOW_TVM_LONDON; + return current().allowTvmLondon; } public static boolean allowTvmCompatibleEvm() { - return ALLOW_TVM_COMPATIBLE_EVM; + return current().allowTvmCompatibleEvm; } public static boolean allowHigherLimitForMaxCpuTimeOfOneTx() { - return ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX; + return current().allowHigherLimitForMaxCpuTimeOfOneTx; } public static boolean allowTvmFreezeV2() { - return ALLOW_TVM_FREEZE_V2; + return current().allowTvmFreezeV2; } public static boolean allowOptimizedReturnValueOfChainId() { - return ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID; + return current().allowOptimizedReturnValueOfChainId; } public static boolean allowDynamicEnergy() { - return ALLOW_DYNAMIC_ENERGY; + return current().allowDynamicEnergy; } public static long getDynamicEnergyThreshold() { - return DYNAMIC_ENERGY_THRESHOLD; + return current().dynamicEnergyThreshold; } public static long getDynamicEnergyIncreaseFactor() { - return DYNAMIC_ENERGY_INCREASE_FACTOR; + return current().dynamicEnergyIncreaseFactor; } public static long getDynamicEnergyMaxFactor() { - return DYNAMIC_ENERGY_MAX_FACTOR; + return current().dynamicEnergyMaxFactor; } public static boolean allowTvmShanghai() { - return ALLOW_TVM_SHANGHAI; + return current().allowTvmShanghai; } public static boolean allowEnergyAdjustment() { - return ALLOW_ENERGY_ADJUSTMENT; + return current().allowEnergyAdjustment; } public static boolean allowStrictMath() { - return ALLOW_STRICT_MATH; + return current().allowStrictMath; } public static boolean allowTvmCancun() { - return ALLOW_TVM_CANCUN; + return current().allowTvmCancun; } public static boolean disableJavaLangMath() { - return DISABLE_JAVA_LANG_MATH; + return current().disableJavaLangMath; } public static boolean allowTvmBlob() { - return ALLOW_TVM_BLOB; + return current().allowTvmBlob; } public static boolean allowTvmSelfdestructRestriction() { - return ALLOW_TVM_SELFDESTRUCT_RESTRICTION; + return current().allowTvmSelfdestructRestriction; } public static boolean allowTvmOsaka() { - return ALLOW_TVM_OSAKA; + return current().allowTvmOsaka; } public static boolean allowHardenResourceCalculation() { - return ALLOW_HARDEN_RESOURCE_CALCULATION; + return current().allowHardenResourceCalculation; } } diff --git a/common/src/main/java/org/tron/json/JSON.java b/common/src/main/java/org/tron/json/JSON.java index 88678c49a44..571b9515ade 100644 --- a/common/src/main/java/org/tron/json/JSON.java +++ b/common/src/main/java/org/tron/json/JSON.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.tron.common.parameter.CommonParameter; +import org.tron.core.Constant; /** * Drop-in replacement for {@code com.alibaba.fastjson.JSON}. @@ -22,15 +22,6 @@ @Deprecated public final class JSON { - // Initialization-order invariant: this class must NOT be loaded before - // Args.setParam() completes. The factory's StreamReadConstraints are a - // one-shot snapshot of CommonParameter at class-init time. If JSON is - // touched too early — e.g. a stray reference in startup code or in a static - // initializer that runs before Args — the snapshot captures CommonParameter's - // hardcoded defaults (100 / 100_000) and any user override of - // node.http.maxNestingDepth / maxTokenCount is silently ignored. - // Current production startup (FullNode.main) calls Args.setParam first and - // no path in that call chain references this class, so the invariant holds. static final ObjectMapper MAPPER = JsonMapper.builder(buildFactory()) // Fastjson Feature.AllowUnQuotedFieldNames (default ON) .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) @@ -67,9 +58,9 @@ public final class JSON { .build(); private static JsonFactory buildFactory() { - CommonParameter p = CommonParameter.getInstance(); return JsonFactory.builder().streamReadConstraints(StreamReadConstraints.builder() - .maxNestingDepth(p.getMaxNestingDepth()).maxTokenCount(p.getMaxTokenCount()) + .maxNestingDepth(Constant.MAX_NESTING_DEPTH) + .maxTokenCount(Constant.MAX_TOKEN_COUNT) .build()).build(); } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 688e1590788..25fc4832e55 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -25,91 +25,103 @@ # Key naming rules (required for ConfigBeanFactory auto-binding): # - Use standard camelCase: maxConnections, syncFetchBatchNum, etc. # -# Keys that cannot auto-bind (handled manually in bean fromConfig): -# -# 1. committee.pBFTExpireNum — lowercase "p" then uppercase "BFT": -# setPBFTExpireNum -> property "PBFTExpireNum" (capital P), -# mismatches config key "pBFTExpireNum" (lowercase p). -# -# 2. node.isOpenFullTcpDisconnect — boolean "is" prefix: -# getter isOpenFullTcpDisconnect() -> property "openFullTcpDisconnect", -# mismatches config key "isOpenFullTcpDisconnect". -# -# 3. node.shutdown.BlockTime/BlockHeight/BlockCount — PascalCase keys: -# setBlockTime -> property "blockTime", mismatches "BlockTime". -# # ============================================================================= +# 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" - # Index directory (legacy, not consumed by any runtime code, kept for CLI/test compatibility) - index.directory = "index" - index.switch = "on" + 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 - # Example per-database overrides: + # 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 4 properties (blockSize, writeBufferSize, cacheSize, maxOpenFiles) + # only take effect when using LevelDB. + # Example: + # properties = [ # { # name = "account", # path = "storage_directory_test", - # createIfMissing = true, - # 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 - # } + # }, + # ] 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 @@ -126,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 @@ -139,60 +152,67 @@ node.discovery = { # BlockCount = 12 # block sync count after node start # } +# Backup node election settings. node.backup { - port = 10001 - priority = 0 - keepAliveInterval = 3000 + 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", - # "ip" + # "ip", # Peer IP list, better to add at most one IP; must not contain this node's own IP ] } # Algorithm for generating public key from private key. Do not modify to avoid forks. crypto { - engine = "eckey" + engine = "eckey" # Signature engine. } # Energy limit block number (config key has typo "enery" preserved for backward compatibility) enery.limit.block.num = 4727890 +# Node metrics 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"). - # Empty string here = "not configured"; Args.java bridge converts "" → null so the - # runtime behavior matches develop (trustNodeAddr is null unless user sets the key). trustNode = "" # Expose extension api to public or not 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 # Max in-flight (requested but not yet processed) blocks during sync. Range: [50, 2000]. maxPendingBlockSize = 500 - # Number of validate sign threads, default availableProcessors # Number of validate sign threads, 0 = auto (availableProcessors) validateSignThreadNum = 0 - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - maxConnectionsWithSameIp = 2 - maxHttpConnectNumber = 50 - minParticipationRate = 0 - - # Whether to enable shielded transaction API + maxConnections = 30 # Maximum peer connections. + minConnections = 8 # Minimum peer connections to maintain. + minActiveConnections = 3 # Minimum active peer connections. + # Legacy alias `maxActiveNodesWithSameIp` is still accepted but deprecated + 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. + # It is recommended to invoke them locally for development and testing. + # To opt in, set: allowShieldedTransactionApi = true + # Migration: the legacy key node.fullNodeAllowShieldedTransaction is still supported + # but deprecated; please migrate to node.allowShieldedTransactionApi. # allowShieldedTransactionApi = false # Whether to print config log at startup @@ -208,76 +228,82 @@ 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 - activeConnectFactor = 0.1 - connectFactor = 0.6 - # 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`. + maxFastForwardNum = 4 # Number of SRs after the block-producing SR that a fast-forward node forwards blocks to. + + # Deprecated. Enables legacy MetricsUtil counters/histograms and registers + # the monitor gRPC service (MonitorApi) on the RPC server. + # This is independent of the Prometheus exporter configured under node.metrics.prometheus. 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 - - # Maximum HTTP request body size, default 4MB. Independent from rpc.maxMessageSize. - maxMessageSize = 4M - maxNestingDepth = 100 - maxTokenCount = 100000 + 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 # Maximum concurrent calls per incoming connection - # No limit on concurrent calls per connection - maxConcurrentCallsPerConnection = 2147483647 + # 0 means No limit on concurrent calls per connection + maxConcurrentCallsPerConnection = 0 # HTTP/2 flow control window (bytes), default 1MB flowControlWindow = 1048576 - # Connection idle timeout (ms). No limit by default. - maxConnectionIdleInMillis = 9223372036854775807 + # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = no limit + maxConnectionIdleInMillis = 0 - # Connection max age (ms). No limit by default. - maxConnectionAgeInMillis = 9223372036854775807 + # Connection max age (ms). 0 = no limit + maxConnectionAgeInMillis = 0 - # Maximum message size (bytes), default 4MB + # Maximum gRPC message size in bytes (default 4194304, ~4MB). + # Must be a non-negative integer. Setting to 0 rejects all non-empty messages. maxMessageSize = 4194304 # Maximum header list size (bytes), default 8192 @@ -294,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. @@ -320,19 +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 - receiveTcpMinDataLength = 2048 maxTransactionPendingSize = 2000 - pendingTransactionTimeout = 60000 + pendingTransactionTimeout = 60000 # Pending transaction timeout (ms). # total cached trx across handler queues + pending + rePush maxTrxCacheSize = 50000 @@ -341,40 +366,64 @@ 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 = [ # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", ] + + # Enable or disable DNS publish. Default: false. publish = false + # DNS domain to publish nodes, required if publish is true. dnsDomain = "" + # DNS private key used to publish, required if publish is true, hex string of length 64. dnsPrivate = "" + # Known DNS URLs to publish if publish is true, format: tree://{pubkey}@{domain}. Default: empty. knownUrls = [] + # Static nodes to publish on DNS, "ip:port". staticNodes = [] - maxMergeSize = 0 - changeThreshold = 0.0 + # Merge several nodes into a leaf of tree, should be 1~5. + maxMergeSize = 5 + # Only update DNS data when node change percent exceeds this threshold. + changeThreshold = 0.1 + # DNS server to publish, required if publish is true. Supported values: "aws", "aliyun". serverType = "" + # Access key ID of AWS or Aliyun API, required if publish is true. accessKeyId = "" + # Access key secret of AWS or Aliyun API, required if publish is true. accessKeySecret = "" + # Endpoint of Aliyun DNS server, required if serverType is "aliyun". aliyunDnsEndpoint = "" + # Region of AWS API (e.g. "us-east-1"), required if serverType is "aws". awsRegion = "" + # Host zone ID of AWS domain, required if serverType is "aws". awsHostZoneId = "" } # Open history query APIs on lite FullNode (may return null for some queries) openHistoryQueryWhenLiteFN = false + # 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 # 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 @@ -390,8 +439,8 @@ node { maxResponseSize = 26214400 # Allowed maximum number for newFilter, <=0 means no limit maxLogFilterNum = 20000 - # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize. - maxMessageSize = 4M + # Maximum JSON-RPC request body size in bytes (default 4194304, ~4MB). Independent from rpc.maxMessageSize. + maxMessageSize = 4194304 } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. @@ -403,9 +452,18 @@ node { ## Rate limiter config rate.limiter = { - # Strategies: GlobalPreemptibleAdapter, QpsRateLimiterAdapter, IPQPSRateLimiterAdapter - # Default: QpsRateLimiterAdapter with qps=1000 - + # Each HTTP servlet and gRPC method can have its own rate-limit strategy. + # Three API rate-limit strategies are available: + # GlobalPreemptibleAdapter – limits maximum concurrent requests globally. + # paramString = "permit=N" (N = max concurrent calls) + # QpsRateLimiterAdapter – limits average QPS across all callers. + # paramString = "qps=N" (N may be a decimal) + # IPQPSRateLimiterAdapter – limits average QPS per source IP. + # paramString = "qps=N" (N may be a decimal) + # If no strategy is configured for an endpoint, QpsRateLimiterAdapter with + # qps=1000 is applied automatically. + + # Per-servlet HTTP rate limits. component is the servlet class simple name. http = [ # { # component = "GetNowBlockServlet", @@ -424,6 +482,7 @@ rate.limiter = { # } ] + # Per-method gRPC rate limits. component is "package.ServiceName/MethodName". rpc = [ # { # component = "protocol.Wallet/GetBlockByLatestNum2", @@ -442,18 +501,28 @@ rate.limiter = { # } ] + # P2P message rate limits. p2p = { - syncBlockChain = 3.0 - fetchInvData = 3.0 - disconnect = 1.0 + # QPS ceiling for individual P2P message types received from peers. + # Values are doubles; fractional QPS is allowed (e.g. 0.5 = one per 2 s). + syncBlockChain = 3.0 # SyncBlockChain handshake messages + fetchInvData = 3.0 # FetchInvData (block/tx fetch) messages + disconnect = 1.0 # Disconnect messages } + # Node-wide QPS ceiling across all HTTP + gRPC requests combined. global.qps = 50000 + # Per-source-IP QPS ceiling across all HTTP + gRPC requests from that IP. global.ip.qps = 10000 + # Default per-endpoint QPS limit applied to any endpoint with no explicit strategy. global.api.qps = 1000 + # true = reject over-limit requests immediately; false = queue and block the caller. + apiNonBlocking = false } +# Bootstrap seed node settings. seed.node = { + # Seed node addresses. ip.list = [ "3.225.171.164:18888", "52.8.46.215:18888", @@ -490,13 +559,27 @@ seed.node = { ] } +## Genesis block config +# WARNING: All nodes in the same network must have identical genesis.block settings. +# Any change here produces a different genesis block hash and creates an incompatible chain. genesis.block = { + # Pre-allocated accounts created at block 0, before any transactions are processed. + # Fields: + # accountName – human-readable label stored on-chain; must not be blank + # accountType – one of: Normal, AssetIssue, Contract + # address – Base58Check-encoded account address (T...) + # balance – initial balance in SUN (1 TRX = 1,000,000 SUN); stored as String + # to accommodate values that exceed Integer range + # Mainnet special accounts: + # Zion – holds the initial circulating supply (99,000,000,000 TRX = 99×10¹⁵ SUN) + # Sun – the founding account; starts at 0 + # 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" @@ -512,11 +595,17 @@ genesis.block = { } ] + # Initial Super Representatives at block 0. + # Fields: + # address – Base58Check-encoded SR address (T...) + # url – SR's public URL (informational only, stored on-chain) + # voteCount – initial vote count; seeds SR ranking before any user votes are cast + # 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, @@ -650,14 +739,23 @@ genesis.block = { } ] + # Genesis block timestamp in milliseconds since Unix epoch. Must be >= 0. + # Stored as a numeric String to accommodate Long-range values. timestamp = "0" + # Hash of the genesis block's conceptual parent. This is a fixed sentinel value + # embedded in the genesis block header; changing it changes the genesis block hash + # and therefore the chain identity. parentHash = "0xe58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f" } # Optional. Used when the witness account has set witnessPermission. +# When it is not empty, the localWitnessAccountAddress represents the address of the witness account, +# and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. +# When it is empty,the localwitness is configured with the private key of the witness account. # localWitnessAccountAddress = +# Local witness private keys. localwitness = [ ] @@ -665,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) @@ -678,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 @@ -701,91 +801,113 @@ vm = { # Max retry time for executing transaction in estimating energy estimateEnergyMaxRetry = 3 + + # Max TVM execution time (ms) for constant calls — applies to + # triggerconstantcontract, triggersmartcontract dispatched to view/pure + # functions, estimateenergy, eth_call, eth_estimateGas, and any other RPC + # routed through the constant-call path. When set, must be a positive + # integer that fits VM deadline conversion and is used verbatim as the + # per-call deadline (no clamp against the network's maxCpuTimeOfOneTx). + # Omit the property entirely to keep the default behaviour of sharing the + # block-processing deadline. Migration note: if previously running --debug + # to extend constant calls, switch to this option (--debug also extends + # block-processing, which is unsafe; see issue #6266). Default: 0 (no effect). + 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 = true - bindport = 5555 - sendqueuelength = 1000 + 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 - path = "" - server = "" + path = "" // absolute path of plugin + server = "" // target server address to receive event triggers, "ip:port" + # 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" - enable = false - topic = "block" - solidified = false + triggerName = "block" // block trigger, the value can't be modified + 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 }, { triggerName = "transaction" enable = false topic = "transaction" solidified = false + // if set true, add transactionIndex, cumulativeEnergyUsed, preCumulativeLogCount, logList, energyUnitPrice. + // Default: false ethCompatible = false }, { - triggerName = "contractevent" + triggerName = "contractevent" // contractevent represents contractlog data decoded by the ABI. enable = false topic = "contractevent" }, @@ -793,11 +915,11 @@ event.subscribe = { triggerName = "contractlog" enable = false topic = "contractlog" - redundancy = false + redundancy = false // if set true, contractevent will also be regarded as contractlog }, { - triggerName = "solidity" - enable = true + triggerName = "solidity" // solidity block trigger (just block number and timestamp), the value can't be modified + enable = false topic = "solidity" }, { @@ -809,16 +931,19 @@ event.subscribe = { triggerName = "soliditylog" enable = false topic = "soliditylog" - redundancy = false + redundancy = false // if set true, solidityevent will also be regarded as soliditylog } ] + # Event filter settings. filter = { - fromblock = "" - toblock = "" + 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 topics to subscribe; "" means any contract topic. contractTopic = [ "" ] diff --git a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java index 962b6a349ab..559198100fb 100644 --- a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java @@ -20,8 +20,8 @@ private static Config withRef() { public void testDefaults() { CommitteeConfig cc = CommitteeConfig.fromConfig(withRef()); assertEquals(0, cc.getAllowCreationOfContracts()); - assertEquals(0, cc.getAllowPBFT()); - assertEquals(20, cc.getPBFTExpireNum()); + assertEquals(0, cc.getAllowPbft()); + assertEquals(20, cc.getPbftExpireNum()); assertEquals(0, cc.getUnfreezeDelayDays()); assertEquals(0, cc.getAllowDynamicEnergy()); } @@ -32,8 +32,8 @@ public void testFromConfig() { "committee { allowCreationOfContracts = 1, allowPBFT = 1, pBFTExpireNum = 30 }"); CommitteeConfig cc = CommitteeConfig.fromConfig(config); assertEquals(1, cc.getAllowCreationOfContracts()); - assertEquals(1, cc.getAllowPBFT()); - assertEquals(30, cc.getPBFTExpireNum()); + assertEquals(1, cc.getAllowPbft()); + assertEquals(30, cc.getPbftExpireNum()); } @Test diff --git a/common/src/test/java/org/tron/core/config/args/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/EventConfigTest.java b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java index 361d9f48581..ca0cbefaddd 100644 --- a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -79,4 +79,11 @@ public void testFilter() { assertEquals(2, ec.getFilter().getContractAddress().size()); assertEquals(1, ec.getFilter().getContractTopic().size()); } + + @Test + public void testTopicsEmptyList() { + EventConfig ec = EventConfig.fromConfig(withRef( + "event.subscribe.topics = []")); + assertTrue(ec.getTopics().isEmpty()); + } } diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index d4fbc05e730..bbc2d2475ee 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -245,8 +245,6 @@ public void testValidContractProtoThreadsExplicitPreserved() { @Test public void testTrustNodeNotDefaultedByReferenceConf() { - // reference.conf intentionally omits `node.trustNode` so that empty configs - // preserve develop's behavior (trustNodeAddr stays null in the Args bridge). NodeConfig nc = NodeConfig.fromConfig(withRef()); assertTrue(nc.getTrustNode() == null || nc.getTrustNode().isEmpty()); } diff --git a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java index 7b4d8a87d45..c3b827a8ba4 100644 --- a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java @@ -1,6 +1,7 @@ package org.tron.core.config.args; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.typesafe.config.Config; @@ -29,6 +30,7 @@ public void testDefaults() { assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001); assertTrue(rl.getHttp().isEmpty()); assertTrue(rl.getRpc().isEmpty()); + assertFalse(rl.isApiNonBlocking()); } @Test @@ -40,7 +42,8 @@ public void testFromConfig() { + " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter," + " paramString = \"qps=10\" }]," + " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter," - + " paramString = \"permit=1\" }]" + + " paramString = \"permit=1\" }]," + + " apiNonBlocking = true" + "}"); RateLimiterConfig rl = RateLimiterConfig.fromConfig(config); assertEquals(100, rl.getGlobal().getQps()); @@ -50,5 +53,6 @@ public void testFromConfig() { assertEquals("TestServlet", rl.getHttp().get(0).getComponent()); assertEquals(1, rl.getRpc().size()); assertEquals("TestRpc", rl.getRpc().get(0).getComponent()); + assertTrue(rl.isApiNonBlocking()); } } diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index ecb956e406a..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 { @@ -26,7 +29,6 @@ public void testDefaults() { assertEquals("LEVELDB", sc.getDb().getEngine()); assertFalse(sc.getDb().isSync()); assertEquals("database", sc.getDb().getDirectory()); - assertEquals("index", sc.getIndex().getDirectory()); assertTrue(sc.isNeedToUpdateAsset()); assertEquals(7, sc.getDbSettings().getLevelNumber()); assertEquals(5000, sc.getDbSettings().getMaxOpenFiles()); @@ -135,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/common/src/test/java/org/tron/core/config/args/VmConfigTest.java b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java index e406ef24e7b..99015a8c012 100644 --- a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -2,10 +2,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import org.junit.Assert; import org.junit.Test; public class VmConfigTest { @@ -90,8 +92,8 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { } // =========================================================================== - // Constant-call timeout (issue #6681). The validation rule: any positive - // value that fits VM deadline conversion is accepted, but zero/negative is + // Constant-call timeout (issue #6681). The validation rule: any zero or positive + // value that fits VM deadline conversion is accepted, but negative is // rejected ONLY when the operator explicitly set the property in their // config. Absence keeps the in-Java default (0L = "share the // block-processing deadline"). @@ -99,7 +101,7 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { @Test public void testConstantCallTimeoutDefaultWhenAbsent() { - // No path in the config, no entry in reference.conf -> default 0L kept, + // reference.conf default is 0; absence of a user override keeps that default; // no validation triggered. VmConfig vm = VmConfig.fromConfig(withRef()); assertEquals(0L, vm.getConstantCallTimeoutMs()); @@ -107,6 +109,8 @@ public void testConstantCallTimeoutDefaultWhenAbsent() { @Test public void testConstantCallTimeoutAcceptsAnyPositiveValue() { + assertEquals(0L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 0 }")).getConstantCallTimeoutMs()); assertEquals(1L, VmConfig.fromConfig( withRef("vm { constantCallTimeoutMs = 1 }")).getConstantCallTimeoutMs()); assertEquals(50L, VmConfig.fromConfig( @@ -117,39 +121,20 @@ public void testConstantCallTimeoutAcceptsAnyPositiveValue() { withRef("vm { constantCallTimeoutMs = 5000 }")).getConstantCallTimeoutMs()); } - @Test - public void testConstantCallTimeoutZeroRejectedWhenExplicitlyConfigured() { - // Operator wrote `= 0` in config -> treated as a misconfiguration even - // though it equals the in-Java default. Forces an explicit positive value. - try { - VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = 0 }")); - org.junit.Assert.fail("expected IllegalArgumentException for explicit 0"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("constantCallTimeoutMs")); - } - } - @Test public void testConstantCallTimeoutNegativeRejected() { - try { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = -1 }")); - org.junit.Assert.fail("expected IllegalArgumentException for negative ms"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("constantCallTimeoutMs")); - } + }); + Assert.assertTrue(thrown.getMessage().contains("constantCallTimeoutMs")); } @Test public void testConstantCallTimeoutOverflowRejected() { - long value = VmConfig.MAX_CONSTANT_CALL_TIMEOUT_MS + 1L; - try { + long value = Long.MAX_VALUE / 1000 + 1L; + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = " + value + " }")); - org.junit.Assert.fail("expected IllegalArgumentException for overflowing ms"); - } catch (IllegalArgumentException ex) { - org.junit.Assert.assertTrue(ex.getMessage(), - ex.getMessage().contains("deadline conversion")); - } + }); + Assert.assertTrue(thrown.getMessage().contains("deadline conversion")); } } diff --git a/crypto/src/main/java/org/tron/common/crypto/SignUtils.java b/crypto/src/main/java/org/tron/common/crypto/SignUtils.java index b921d548e8b..e0e20fb2677 100644 --- a/crypto/src/main/java/org/tron/common/crypto/SignUtils.java +++ b/crypto/src/main/java/org/tron/common/crypto/SignUtils.java @@ -1,5 +1,8 @@ package org.tron.common.crypto; +import static org.tron.core.Constant.MAX_PER_SIGN_LENGTH; +import static org.tron.core.Constant.PER_SIGN_LENGTH; + import java.security.SecureRandom; import java.security.SignatureException; import org.tron.common.crypto.ECKey.ECDSASignature; @@ -8,6 +11,21 @@ public class SignUtils { + /** + * Strict signature-length check for admission entry-points (RPC broadcast, + * P2P transaction ingress, peer hello handshake). Accepts only sizes in + * [{@link org.tron.core.Constant#PER_SIGN_LENGTH PER_SIGN_LENGTH}, + * {@link org.tron.core.Constant#MAX_PER_SIGN_LENGTH MAX_PER_SIGN_LENGTH}]. + * + *

Consensus paths (e.g. {@code TransactionCapsule.checkWeight}) intentionally + * keep the looser {@code size < 65} check to remain compatible with historical + * on-chain signatures that carry trailing padding bytes; do not call this + * helper from those paths. + */ + public static boolean isValidLength(int size) { + return size >= PER_SIGN_LENGTH && size <= MAX_PER_SIGN_LENGTH; + } + public static SignInterface getGeneratedRandomSign( SecureRandom secureRandom, boolean isECKeyCryptoEngine) { if (isECKeyCryptoEngine) { diff --git a/docs/configuration-conventions.md b/docs/configuration-conventions.md new file mode 100644 index 00000000000..c9265b9544e --- /dev/null +++ b/docs/configuration-conventions.md @@ -0,0 +1,226 @@ +# HOCON Configuration Conventions for Developers + +This document covers the rules and patterns that developers must follow when adding or modifying configuration parameters in java-tron. Violations cause silent misreads, startup failures, or hard-to-diagnose defaults being applied instead of user-supplied values. + +## Configuration Parameter vs. Constant: Which One to Use? + +Before writing any code, decide whether the value belongs in a config file or in source code as a constant. Getting this wrong creates dead configuration surface (parameters that exist but are never tuned) or inflexibility (values that should be adjustable but aren't). + +### Use a configuration parameter when + +- **Different deployments legitimately need different values.** Port numbers, peer lists, storage paths, block-production timeouts, and rate limits vary by environment (mainnet / testnet / private chain) or by hardware capacity. +- **Operators may need to tune the value without rebuilding.** Examples: thread pool sizes, connection limits, QPS caps. +- **The value is an on/off feature flag with production-safe semantics.** The flag must be safe to flip while the rest of the system is unchanged (e.g. `node.rpc.reflectionService`, `vm.estimateEnergy`). +- **The default differs across deployment scenarios.** If the mainnet default and the private-chain default are different, it belongs in config so each can override. + +### Use a constant when + +- **No operator would ever need to change it.** Protocol-level numbers (address prefix bytes, transaction size ceilings, energy unit ratios) are part of the chain specification — changing them causes a fork. +- **The value is a technical limit determined by the implementation, not the deployment.** Jackson `StreamReadConstraints` (`MAX_NESTING_DEPTH`, `MAX_TOKEN_COUNT`) guard against malformed input; no legitimate request comes close to the limit and no operator tunes it. +- **The "configurability" is an illusion.** If the value is captured in a `static final` field at class-load time (before config is applied), a config key is misleading — it appears tunable but changes are silently ignored. Convert to a constant and document why. +- **The value is derived from other constants or from the Java runtime.** Use `Runtime.getRuntime().availableProcessors()` or arithmetic on existing constants; don't push the formula into a config file. +- **No code path reads the value after assignment.** A parameter that exists in `reference.conf` and propagates through `NodeConfig → Args → CommonParameter` but is never consumed by business logic is dead weight. Delete it entirely (see `receiveTcpMinDataLength` as a past example). + +### The warning signs of a misplaced parameter + +| Symptom | Likely problem | +|---------|---------------| +| Parameter exists in `reference.conf` but `grep` finds no call site beyond the binding chain | Dead parameter — delete it | +| Value is read from a `static final` field initialized before `Args.setParam()` | Config change is silently ignored — convert to constant | +| Operator sets the value and nothing changes | Same as above, or value is clamped away in `postProcess()` | +| Parameter controls something that would cause a network fork if mismatched across nodes | Must be a constant, not configurable | +| Parameter has been at its default value in every known deployment for over a year | Candidate for removal or promotion to constant | + +## How Config Keys Bind to Java Fields + +java-tron uses [Typesafe Config](https://github.com/lightbend/config)'s `ConfigBeanFactory` to map a HOCON section to a Java bean automatically. The mapping algorithm is: + +1. For each field `fooBar` in the bean, `ConfigBeanFactory` looks for a HOCON key named `fooBar`. +2. The bean class must expose a public setter (`setFooBar`) — in practice this is provided by Lombok `@Setter`. +3. If the key is absent from the config, the field keeps its Java default value (the one assigned in the field declaration). +4. If the key is present but the type does not match, binding fails with a `ConfigException` at startup. + +The binding entry point for each top-level section looks like: + +```java +// "node" section → NodeConfig bean +Config section = config.getConfig("node"); +NodeConfig nc = ConfigBeanFactory.create(section, NodeConfig.class); +``` + +## Key Naming: Use camelCase + +**All keys in `reference.conf` and `config.conf` must use standard camelCase.** + +`ConfigBeanFactory` derives the expected key name from the Java setter via the JavaBean Introspector: `setFooBar` → property name `fooBar` → expected HOCON key `fooBar`. If the key in the config file uses a different casing, the binding silently skips it and the field keeps its Java default. + +```hocon +# Correct +node { + maxConnections = 30 + syncFetchBatchNum = 2000 +} + +# Wrong — ConfigBeanFactory cannot find these +node { + MaxConnections = 30 # PascalCase → ignored + sync_fetch_batch_num = 2000 # snake_case → ignored + max-connections = 30 # kebab-case → ignored +} +``` + +### The PBFT Exception + +Two legacy keys under `committee` (`allowPBFT`, `pBFTExpireNum`) and the HTTP/RPC fields (`PBFTEnable`, `PBFTPort`) were introduced with non-standard casing before this rule was established. They are retained as-is in the config files for backward compatibility. **Do not model new keys after them.** + +For `allowPBFT` and `pBFTExpireNum`, `CommitteeConfig.normalizeNonStandardKeys()` renames them to proper camelCase (`allowPbft`, `pbftExpireNum`) before handing the section to `ConfigBeanFactory`. If you ever need to accept a non-standard key from users while binding to a standard field, follow this same pattern. + +### The `is` Prefix Exception + +A HOCON key named `isOpenFullTcpDisconnect` produces the setter `setIsOpenFullTcpDisconnect`, but the JavaBean Introspector derives the property name as `openFullTcpDisconnect` (stripping `is`), so `ConfigBeanFactory` looks for key `openFullTcpDisconnect`. `NodeConfig.normalizeNonStandardKeys()` renames the key at read time for backward compatibility. **Do not add new keys with an `is` prefix.** + +## Nesting Depth + +The CI gate enforces a hard ceiling of **5 levels** (the historical maximum in `reference.conf`). New parameters should stay within **3 levels** from the top-level section. The gap between 3 and 5 is reserved for legacy paths that already exist — it is not a license to add new deep keys. + +``` +level 1: node { ... } +level 2: node { rpc { ... } } +level 3: node { rpc { flowControl { ... } } } ← limit for new keys +level 4+: node { rpc { flowControl { window { ... } } } } ← legacy only; do not add new keys here +level 6+: rejected by CI gate unconditionally +``` + +Each level of nesting requires a corresponding inner static bean class. If you find yourself going beyond 3 levels deep, consider flattening by moving the leaf keys up one level or using a longer camelCase key at level 2. + +## Configuration Loading Order + +java-tron loads configuration in two layers at startup: + +``` +Priority (highest wins): + 1. User config file — passed via -c; replaces the bundled config.conf entirely + 2. reference.conf — always loaded from inside the jar; provides defaults for every key +``` + +When a user passes `-c /path/to/node.conf`, the bundled `config.conf` is **not loaded at all** — it is completely replaced by the user's file. `reference.conf` is the only built-in file that is guaranteed to be read in every deployment. + +When `-c` is omitted (development or quick-start), the bundled `config.conf` fills the same role a user file would: it overrides `reference.conf` defaults for the keys it declares. + +The practical consequence for developers: **the default value you put in `reference.conf` is the value every production node uses.** The bundled `config.conf` only matters for users who start the node without `-c`. + +## Adding a New Parameter: Checklist + +When adding a configuration parameter, all four steps are required in the same commit. + +### Step 1 — Add the key to `reference.conf` with its default value + +`reference.conf` (in `common/src/main/resources/`) must contain every key the code reads. This is the single source of truth for defaults. Add a brief inline comment explaining the key's purpose and valid range. + +```hocon +node { + # Maximum number of transaction verifier threads. 0 = auto (availableProcessors). + myNewOption = 0 +} +``` + +### Step 2 — Add the field to the corresponding bean class + +Add a field whose name **exactly matches** the HOCON key, with the same default value as `reference.conf`. If the field is in a sub-bean, ensure the sub-bean is mapped correctly. + +```java +// NodeConfig.java +private int myNewOption = 0; // 0 = auto +``` + +Lombok `@Getter` and `@Setter` on the class provide the accessor methods that `ConfigBeanFactory` needs. Do not write them by hand. + +### Step 3 — Add clamping / validation in `postProcess()` if needed + +Every bean's `postProcess()` (called from `fromConfig()` after binding) is where out-of-range values are clamped and cross-field invariants are enforced. Do not add defensive checks scattered through the rest of the codebase. + +```java +// in NodeConfig.postProcess() +if (myNewOption == 0) { + myNewOption = Runtime.getRuntime().availableProcessors(); +} +if (myNewOption > 64) { + myNewOption = 64; +} +``` + +### Step 4 — Add the key to `config.conf` only if the default is intentionally different + +`config.conf` (in `framework/src/main/resources/`) is the sample user config shipped with the distribution. Only add your new key there if the value users should start with differs from the `reference.conf` default, or if the key needs a visible comment for users. + +Remember: in any real deployment the user passes `-c` and the bundled `config.conf` is bypassed entirely (see [Configuration Loading Order](#configuration-loading-order)). `reference.conf` is where your default actually takes effect — make sure it is safe and correct before touching `config.conf`. + +## Field Types and HOCON Value Types + +| Java field type | HOCON value | Notes | +|-------------------|-------------|-------| +| `boolean` | `true` / `false` | | +| `int` / `long` | numeric | Must be a plain integer; human-readable sizes (`4m`, `128MB`) are not supported | +| `double` | numeric | | +| `String` | `"value"` | Null HOCON values must be normalized to `""` before binding (see `normalizeNonStandardKeys`) | +| `List` | `["a", "b"]` | Must be read manually; `ConfigBeanFactory` does not handle `List` | +| Inner bean | `{ key = val }` | The Java field type must be the inner static class | + +### List Fields + +`ConfigBeanFactory` handles `List` but not `List`. Read string-list fields manually after `ConfigBeanFactory.create()`: + +```java +NodeConfig nc = ConfigBeanFactory.create(section, NodeConfig.class); +nc.active = section.getStringList("active"); +``` + +## Backward Compatibility and Legacy Keys + +When renaming a key, keep reading the old key as a fallback for at least one major release: + +```java +// fromConfig() — after ConfigBeanFactory binding +if (section.hasPath("oldKeyName")) { + nc.newFieldName = section.getInt("oldKeyName"); + logger.warn("Config key [section.oldKeyName] is deprecated; use [section.newKeyName]."); +} +``` + +Never remove the old key from this fallback read without a deprecation period and a release note. + +## Optional Keys (Not in `reference.conf`) + +Most keys should be in `reference.conf`. Use optional keys (absent from `reference.conf`, only read if present) sparingly — only for parameters where the presence/absence itself carries meaning. Read them with `hasPath()` guards and annotate the Java field with `@Setter(lombok.AccessLevel.NONE)` to prevent `ConfigBeanFactory` from requiring the key: + +```java +@Setter(lombok.AccessLevel.NONE) +private String shutdownBlockTime = ""; // "" = not set + +// in fromConfig(), after ConfigBeanFactory.create(): +nc.shutdownBlockTime = section.hasPath("shutdown.BlockTime") + ? section.getString("shutdown.BlockTime") : ""; +``` + +## Key Naming Conventions Summary + +| Rule | Good | Bad | +|------|------|-----| +| Standard camelCase | `maxConnections` | `MaxConnections`, `max_connections`, `max-connections` | +| No `is` prefix | `openFullTcpDisconnect` | `isOpenFullTcpDisconnect` | +| No all-caps acronym prefix | `pbftExpireNum`, `pBFTPort`* | `PBFTExpireNum` | +| New keys: nesting ≤ 3 levels | `node.rpc.maxMessageSize` | `node.rpc.limits.size.max` | +| Java field name matches HOCON key exactly | field `maxConnections` ↔ key `maxConnections` | field `maxConns` ↔ key `maxConnections` | + +\* `PBFTEnable` / `PBFTPort` are legacy exceptions; do not model new keys after them. + +## Where to Find Existing Patterns + +| Pattern | Reference location | +|---------|-------------------| +| Standard flat scalar binding | `VmConfig.java`, `BlockConfig.java` | +| Sub-bean nesting | `NodeConfig.HttpConfig`, `NodeConfig.RpcConfig` | +| Legacy key fallback | `NodeConfig.fromConfig()` (`maxActiveNodes`, `maxActiveNodesWithSameIp`) | +| Non-standard key normalization | `CommitteeConfig.normalizeNonStandardKeys()`, `NodeConfig.normalizeNonStandardKeys()` | +| Optional PascalCase keys | `NodeConfig.fromConfig()` (`shutdown.BlockTime/Height/Count`) | +| `postProcess()` clamping | `NodeConfig.postProcess()`, `CommitteeConfig.postProcess()` | diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000000..28b53b1970c --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,259 @@ +# Node Configuration Guide + +This guide explains the two-layer configuration system used by java-tron and walks through the most common customizations a node operator needs. + +## How Configuration Files Work Together + +java-tron uses [Typesafe Config](https://github.com/lightbend/config) and applies configuration in priority order at startup: + +| File | Location | Purpose | +|------|----------|---------| +| `reference.conf` | Bundled inside the jar (`common` module) | Declares every parameter with its default value | +| Bundled `config.conf` | Bundled inside the jar (`framework` module) | Shipped template; active only when `-c` is omitted | +| Your config file (e.g. `node.conf`) | Operator-supplied, passed via `-c` | Overrides values that differ from defaults; replaces the bundled `config.conf` entirely | + +**Loading priority:** values in your config file always win. Any parameter your file omits is automatically filled in from `reference.conf`. You never need to copy the entire `reference.conf` into your own file — only include the parameters you actually want to change. + +``` +startup resolution order (highest wins): + 1. your config file (passed with -c; replaces bundled config.conf) + 2. bundled config.conf (only when -c is omitted) + 3. reference.conf (always loaded; fallback for every key) +``` + +`reference.conf` is the authoritative source of truth for every parameter name and its default. When in doubt, consult that file to see what a parameter does and what value the node will use if you leave it out. + +## Starting a Node with a Config File + +```bash +# Using the distribution script +java-tron-1.0.0/bin/FullNode -c /path/to/node.conf + +# Using the jar directly +java -jar FullNode.jar -c /path/to/node.conf + +# SR (Super Representative) mode +java-tron-1.0.0/bin/FullNode -c /path/to/node.conf -w +``` + +If `-c` is omitted, the node loads the `config.conf` bundled inside the jar (the same file shipped with the distribution) merged with `reference.conf` as fallback. The bundled file already enables discovery/persist for mainnet operation. For production, copy it out, edit, and pass the edited copy via `-c` to make your configuration visible to operators. + +## Minimal Config File + +Your config file only needs to contain what you want to change. The following is sufficient for a mainnet full node: + +```hocon +node.discovery = { + enable = true + persist = true +} + +node { + listen.port = 18888 + minParticipationRate = 15 + p2p.version = 11111 # mainnet +} + +seed.node.ip.list = [ + "3.225.171.164:18888", + "52.8.46.215:18888", + # ... (see reference.conf for the full seed list) +] +``` + +## Common Configuration Sections + +### Network and P2P (`node`, `node.discovery`, `seed.node`) + +```hocon +node.discovery = { + enable = true # join the peer-discovery network + persist = true # save discovered peers across restarts +} + +node { + listen.port = 18888 # TCP port for peer connections + maxConnections = 30 # maximum peer connections + minConnections = 8 # minimum peer connections to maintain + minParticipationRate = 15 # minimum % of active witnesses before producing blocks + + p2p { + version = 11111 # Mainnet:11111 Nile:201910292 Shasta:1 + } +} + +seed.node.ip.list = [ + "3.225.171.164:18888", + # add more entries as needed +] +``` + +### HTTP and gRPC APIs (`node.http`, `node.rpc`) + +```hocon +node { + http { + fullNodeEnable = true + fullNodePort = 8090 + solidityEnable = true + solidityPort = 8091 + } + + rpc { + enable = true + port = 50051 + solidityEnable = true + solidityPort = 50061 + # Maximum concurrent calls per connection. 0 = no limit. + maxConcurrentCallsPerConnection = 0 + # Idle connection timeout (ms). 0 = no limit. + maxConnectionIdleInMillis = 0 + # Minimum active connections required before broadcasting transactions. + minEffectiveConnection = 1 + } +} +``` + +To disable an API endpoint that you do not want to expose publicly, set its `Enable` flag to `false` or add endpoints to `node.disabledApi`: + +```hocon +node.disabledApi = [ + "getaccount", + "getnowblock2" +] +``` + +### Storage Engine (`storage`) + +```hocon +storage { + db.engine = "LEVELDB" # "LEVELDB" or "ROCKSDB"; ARM64 requires "ROCKSDB" + db.sync = false # set true for maximum durability (slower writes) + db.directory = "database" +} +``` + +To override the storage path for individual databases: + +```hocon +storage.properties = [ + { + name = "account", + path = "/data/tron/account-db" + } +] +``` + +### Block Production (Super Representatives) + +```hocon +# Plain private key (use localwitnesskeystore for production) +localwitness = [ + "your-private-key-hex" +] + +# Recommended: keystore file +# localwitnesskeystore = [ +# "/path/to/localwitnesskeystore.json" +# ] + +# Required when the witness account has delegated block-signing to a separate key +# localWitnessAccountAddress = "T..." +``` + +### JSON-RPC (Ethereum-compatible, `node.jsonrpc`) + +```hocon +node.jsonrpc { + httpFullNodeEnable = true + httpFullNodePort = 8545 + maxBlockRange = 5000 # max block range for eth_getLogs + maxResponseSize = 26214400 # 25 MB +} +``` + +### Event Subscription (`event.subscribe`) + +```hocon +event.subscribe = { + enable = true + native { + useNativeQueue = true + bindport = 5555 + sendqueuelength = 1000 + } + topics = [ + { triggerName = "block", enable = true, topic = "block" }, + { triggerName = "transaction", enable = true, topic = "transaction" }, + { triggerName = "solidity", enable = true, topic = "solidity" } + ] +} +``` + +### Rate Limiting (`rate.limiter`) + +```hocon +rate.limiter = { + # Available strategies: + # GlobalPreemptibleAdapter — semaphore-based, paramString = "permit=N" + # QpsRateLimiterAdapter — node-wide QPS cap, paramString = "qps=N" + # IPQPSRateLimiterAdapter — per-IP QPS cap, paramString = "qps=N" + + http = [ + { + component = "GetAccountServlet", + strategy = "IPQPSRateLimiterAdapter", + paramString = "qps=10" + } + ] + + global.qps = 50000 + global.ip.qps = 10000 +} +``` + +### Dynamic Config Reload (`node.dynamicConfig`) + +When enabled, the node re-reads your config file periodically without restarting: + +```hocon +node.dynamicConfig = { + enable = true + checkInterval = 600 # seconds between checks +} +``` + +Not all parameters support hot-reload. Parameters that affect node identity, genesis block, or database layout require a full restart. + +## Parameters You Should Not Change + +| Parameter | Reason | +|-----------|--------| +| `crypto.engine` | Changing the key-derivation algorithm will fork the node | +| `genesis.block.*` | Must be identical on every node in the network | +| `committee.*` | Controlled by on-chain governance proposals; manual overrides are for private chains only | +| `node.p2p.version` | Must match the network (11111 for mainnet) | +| `enery.limit.block.num` | Intentional typo preserved for backward compatibility; do not rename | + +## Applying a Config Change + +1. Edit your config file — only add or change the keys you need. +2. If `node.dynamicConfig.enable = true`, wait up to `checkInterval` seconds; the node picks up the change automatically. +3. Otherwise, restart the node: `kill ` then relaunch with the same `-c` flag. +4. Check startup logs for a `[config]` line confirming the file was loaded and watch for any `ERROR` lines about unknown or invalid keys. + +## Viewing Effective Configuration + +At startup, the node unconditionally logs a summary of key parameters under `Net config`, `Backup config`, `Code version`, `DB config`, and `shutDown config` headers (see `Args.logConfig()` for the exact fields). For parameters not in this summary, you must inspect runtime behavior or consult `reference.conf` directly — the full merged configuration is never dumped. + +Note: `node.openPrintLog` is a separate flag that controls runtime verbosity of P2P/inventory/pending-tx logs, not startup config logging. + +## Full Reference + +Every parameter with its default value and an inline comment is documented in: + +``` +common/src/main/resources/reference.conf +``` + +When you need the authoritative default for a parameter or want to understand what a key does, consult that file directly. diff --git a/docs/implement-a-customized-actuator-en.md b/docs/implement-a-customized-actuator-en.md index 76e1852824c..912a49c5d63 100644 --- a/docs/implement-a-customized-actuator-en.md +++ b/docs/implement-a-customized-actuator-en.md @@ -229,7 +229,7 @@ public class SumActuatorTest { @BeforeClass public static void init() throws IOException { Args.setParam(new String[]{"--output-directory", - temporaryFolder.newFolder().toString()}, "config-localtest.conf"); + temporaryFolder.newFolder().toString()}, "config-test.conf"); context = new TronApplicationContext(DefaultConfig.class); appTest = ApplicationFactory.create(context); appTest.startup(); @@ -255,7 +255,7 @@ public class SumActuatorTest { @Test public void sumActuatorTest() { - // this key is defined in config-localtest.conf as accountName=Sun + // this key is defined in config-test.conf as accountName=Sun String key = ""; byte[] address = PublicMethed.getFinalAddress(key); ECKey ecKey = null; diff --git a/docs/implement-a-customized-actuator-zh.md b/docs/implement-a-customized-actuator-zh.md index 1128849916a..9aa0e258127 100644 --- a/docs/implement-a-customized-actuator-zh.md +++ b/docs/implement-a-customized-actuator-zh.md @@ -231,7 +231,7 @@ public class SumActuatorTest { @BeforeClass public static void init() throws IOException { Args.setParam(new String[]{"--output-directory", - temporaryFolder.newFolder().toString()}, "config-localtest.conf"); + temporaryFolder.newFolder().toString()}, "config-test.conf"); context = new TronApplicationContext(DefaultConfig.class); appTest = ApplicationFactory.create(context); appTest.startup(); @@ -257,7 +257,7 @@ public class SumActuatorTest { @Test public void sumActuatorTest() { - // this key is defined in config-localtest.conf as accountName=Sun + // this key is defined in config-test.conf as accountName=Sun String key = ""; byte[] address = PublicMethed.getFinalAddress(key); ECKey ecKey = null; diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 1318fd96527..1dea271ec69 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -16,12 +16,22 @@ package org.tron.common.application; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.ConnectionLimit; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.BufferUtil; import org.tron.core.config.args.Args; @Slf4j(topic = "rpc") @@ -72,6 +82,7 @@ protected void initServer() { if (maxHttpConnectNumber > 0) { this.apiServer.addBean(new ConnectionLimit(maxHttpConnectNumber, this.apiServer)); } + this.apiServer.setErrorHandler(new OversizedRequestErrorHandler()); } protected ServletContextHandler initContextHandler() { @@ -88,4 +99,30 @@ protected ServletContextHandler initContextHandler() { protected void addFilter(ServletContextHandler context) { } + + /** + * For oversized requests (the 413 thrown by SizeLimitHandler during dispatch) logs the + * detail server-side and returns the short, uniform bad-message page, instead of the + * default error page that leaks the exception stack and internal request sizes. All + * other errors keep Jetty's default handling. + */ + private static final class OversizedRequestErrorHandler extends ErrorHandler { + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + if (response.getStatus() == HttpStatus.PAYLOAD_TOO_LARGE_413) { + Throwable cause = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + logger.info("Reject oversized request, uri: {}, detail: {}", + request.getRequestURI(), cause == null ? "413" : cause.getMessage()); + baseRequest.setHandled(true); + ByteBuffer body = badMessageError(HttpStatus.PAYLOAD_TOO_LARGE_413, + HttpStatus.getMessage(HttpStatus.PAYLOAD_TOO_LARGE_413), + baseRequest.getResponse().getHttpFields()); + response.getOutputStream().write(BufferUtil.toArray(body)); + return; + } + super.handle(target, baseRequest, request, response); + } + } } diff --git a/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java index 7061b2e9d57..c0b7afd6779 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java +++ b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java @@ -3,6 +3,7 @@ import com.beust.jcommander.internal.Sets; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; import java.io.File; import java.util.HashSet; import java.util.List; @@ -14,8 +15,10 @@ import org.bouncycastle.util.encoders.Hex; import org.pf4j.CompoundPluginDescriptorFinder; import org.pf4j.DefaultPluginManager; +import org.pf4j.DefaultVersionManager; import org.pf4j.ManifestPluginDescriptorFinder; import org.pf4j.PluginManager; +import org.pf4j.VersionManager; import org.springframework.util.StringUtils; import org.tron.common.logsfilter.nativequeue.NativeMessageQueue; import org.tron.common.logsfilter.trigger.BlockLogTrigger; @@ -29,6 +32,16 @@ @Slf4j public class EventPluginLoader { + /** + * Minimum event-plugin Plugin-Version compatible with this node. Bumped to 3.0.0 to + * reject pre-fastjson-removal builds whose worker threads would fail with + * NoClassDefFoundError on com.alibaba.fastjson at runtime. The previous event-plugin + * release is 2.2.0, so 3.0.0 is the first version that ships the Jackson replacement. + */ + static final String MIN_PLUGIN_VERSION = "3.0.0"; + + private static final VersionManager VERSION_MANAGER = new DefaultVersionManager(); + private static EventPluginLoader instance; private long MAX_PENDING_SIZE = 50000; @@ -457,6 +470,10 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return false; } + if (!isPluginVersionSupported(pluginManager, pluginId)) { + return false; + } + pluginManager.startPlugins(); eventListeners = pluginManager.getExtensions(IPluginEventListener.class); @@ -471,6 +488,21 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return true; } + static boolean isPluginVersionSupported(PluginManager pm, String pluginId) { + String pluginVersion = pm.getPlugin(pluginId).getDescriptor().getVersion(); + if (Strings.isNullOrEmpty(pluginVersion)) { + return false; + } + boolean isSupported = VERSION_MANAGER.compareVersions(pluginVersion, MIN_PLUGIN_VERSION) >= 0; + + if (!isSupported) { + logger.error( + "event-plugin '{}' version {} is older than required {}, please upgrade event-plugin", + pluginId, pluginVersion, MIN_PLUGIN_VERSION); + } + return isSupported; + } + public void stopPlugin() { if (Objects.nonNull(pluginManager)) { pluginManager.stopPlugins(); diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsule.java index b714134ff60..de040d33d44 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsule.java +++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsule.java @@ -27,6 +27,10 @@ public void setLatestSolidifiedBlockNumber(long latestSolidifiedBlockNumber) { blockLogTrigger.setLatestSolidifiedBlockNumber(latestSolidifiedBlockNumber); } + public void setRemoved(boolean removed) { + blockLogTrigger.setRemoved(removed); + } + @Override public void processTrigger() { EventPluginLoader.getInstance().postBlockTrigger(blockLogTrigger); diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/TransactionLogTriggerCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/TransactionLogTriggerCapsule.java index 958b3c0fd78..cf1737d9d22 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/capsule/TransactionLogTriggerCapsule.java +++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/TransactionLogTriggerCapsule.java @@ -377,6 +377,10 @@ public void setLatestSolidifiedBlockNumber(long latestSolidifiedBlockNumber) { transactionLogTrigger.setLatestSolidifiedBlockNumber(latestSolidifiedBlockNumber); } + public void setRemoved(boolean removed) { + transactionLogTrigger.setRemoved(removed); + } + private List getInternalTransactionList( List internalTransactionList) { List pojoList = new ArrayList<>(); diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0482643d8d0..ac54cb2b7ff 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -202,6 +202,7 @@ import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; import org.tron.core.utils.TransactionUtil; +import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; import org.tron.core.zen.ShieldedTRC20ParametersBuilder; import org.tron.core.zen.ShieldedTRC20ParametersBuilder.ShieldedTRC20ParametersType; @@ -228,6 +229,8 @@ import org.tron.protos.Protocol.MarketOrderPairList; import org.tron.protos.Protocol.MarketPrice; import org.tron.protos.Protocol.MarketPriceList; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; import org.tron.protos.Protocol.Proposal; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract; @@ -270,6 +273,8 @@ public class Wallet { "BurnNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])")); private static final byte[] SHIELDED_TRC20_LOG_TOPICS_BURN_TOKEN = Hash.sha3(ByteArray .fromString("TokenBurn(address,uint256,bytes32[3])")); + private static final byte[] SHIELDED_TRC20_LOG_TOPICS_NOTE_SPENT = Hash.sha3(ByteArray + .fromString("NoteSpent(bytes32)")); private static final String BROADCAST_TRANS_FAILED = "Broadcast transaction {} failed, {}."; @Getter @@ -505,6 +510,16 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { trx.setTime(System.currentTimeMillis()); Sha256Hash txID = trx.getTransactionId(); try { + for (ByteString sig : signedTransaction.getSignatureList()) { + if (!SignUtils.isValidLength(sig.size())) { + String info = "Signature size is " + sig.size(); + logger.warn("Broadcast transaction {} has failed, {}.", txID, info); + return builder.setResult(false).setCode(response_code.SIGERROR) + .setMessage(ByteString.copyFromUtf8("Validate signature error: " + info)) + .build(); + } + } + if (tronNetDelegate.isBlockUnsolidified()) { logger.warn("Broadcast transaction {} has failed, block unsolidified.", txID); return builder.setResult(false).setCode(response_code.BLOCK_UNSOLIDIFIED) @@ -556,9 +571,9 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { if (trx.getInstance().getRawData().getContractCount() == 0) { throw new ContractValidateException(ActuatorConstant.CONTRACT_NOT_EXIST); } - TransactionMessage message = new TransactionMessage(trx.getInstance().toByteArray()); trx.checkExpiration(chainBaseManager.getNextBlockSlotTime()); dbManager.pushTransaction(trx); + TransactionMessage message = new TransactionMessage(trx.getInstance().toByteArray()); int num = tronNetService.fastBroadcastTransaction(message); if (num == 0 && minEffectiveConnection != 0) { return builder.setResult(false).setCode(response_code.NOT_ENOUGH_EFFECTIVE_CONNECTION) @@ -616,6 +631,17 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { public TransactionApprovedList getTransactionApprovedList(Transaction trx) { TransactionApprovedList.Builder tswBuilder = TransactionApprovedList.newBuilder(); + TransactionApprovedList.Result.Builder resultBuilder = TransactionApprovedList.Result + .newBuilder(); + if (trx.getSignatureCount() > chainBaseManager.getDynamicPropertiesStore() + .getTotalSignNum()) { + resultBuilder.setCode(TransactionApprovedList.Result.response_code.OTHER_ERROR); + resultBuilder.setMessage("too many signatures"); + tswBuilder.setResult(resultBuilder); + return tswBuilder.build(); + } + + trx = TransactionUtil.truncateSignatures(trx); TransactionExtention.Builder trxExBuilder = TransactionExtention.newBuilder(); trxExBuilder.setTransaction(trx); trxExBuilder.setTxid(ByteString.copyFrom(Sha256Hash.hash(CommonParameter @@ -624,8 +650,6 @@ public TransactionApprovedList getTransactionApprovedList(Transaction trx) { retBuilder.setResult(true).setCode(response_code.SUCCESS); trxExBuilder.setResult(retBuilder); tswBuilder.setTransaction(trxExBuilder); - TransactionApprovedList.Result.Builder resultBuilder = TransactionApprovedList.Result - .newBuilder(); if (trx.getRawData().getContractCount() == 0) { resultBuilder.setCode(TransactionApprovedList.Result.response_code.OTHER_ERROR); @@ -638,21 +662,26 @@ public TransactionApprovedList getTransactionApprovedList(Transaction trx) { if (account == null) { throw new PermissionException("Account does not exist!"); } + int permissionId = contract.getPermissionId(); + Permission permission = account.getPermissionById(permissionId); + if (permission == null) { + throw new PermissionException("Permission for this, does not exist!"); + } + if (permissionId != 0) { + if (permission.getType() != PermissionType.Active) { + throw new PermissionException("Permission type is wrong!"); + } + //check operations + if (!WalletUtil.checkPermissionOperations(permission, contract)) { + throw new PermissionException("Permission denied!"); + } + } if (trx.getSignatureCount() > 0) { - List approveList = new ArrayList(); + List approveList = new ArrayList<>(); byte[] hash = Sha256Hash.hash(CommonParameter .getInstance().isECKeyCryptoEngine(), trx.getRawData().toByteArray()); - for (ByteString sig : trx.getSignatureList()) { - if (sig.size() < 65) { - throw new SignatureFormatException( - "Signature size is " + sig.size()); - } - String base64 = TransactionCapsule.getBase64FromByteString(sig); - byte[] address = SignUtils.signatureToAddress(hash, base64, Args.getInstance() - .isECKeyCryptoEngine()); - approveList.add(ByteString.copyFrom(address)); //out put approve list. - } + TransactionCapsule.checkWeight(permission, trx.getSignatureList(), hash, approveList); tswBuilder.addAllApprovedList(approveList); } resultBuilder.setCode(TransactionApprovedList.Result.response_code.SUCCESS); @@ -3129,8 +3158,14 @@ public Transaction callConstantContract(TransactionCapsule trxCap, StoreFactory.getInstance(), true, false); VMActuator vmActuator = new VMActuator(true); - vmActuator.validate(context); - vmActuator.execute(context); + try { + vmActuator.validate(context); + vmActuator.execute(context); + } finally { + // constant call runs on a pooled RPC worker; drop its thread-local VM config view so it + // can never leak into a later (block/broadcast) execution on the same thread. + VMConfig.clearLocalSnapshot(); + } ProgramResult result = context.getProgramResult(); if (!isEstimating && result.getException() != null @@ -3672,9 +3707,7 @@ public ShieldedTRC20Parameters createShieldedContractParameters( builder.setTransparentToAddress(transparentToAddressTvm); builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); + builder.setOvk(ovk); ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); @@ -3799,9 +3832,7 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk( System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); builder.setTransparentToAddress(transparentToAddressTvm); builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); + builder.setOvk(ovk); GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); if (receiveSize == 1) { @@ -3838,6 +3869,8 @@ private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddr return 3; } else if (Arrays.equals(topicsBytes, SHIELDED_TRC20_LOG_TOPICS_BURN_TOKEN)) { return 4; + } else if (Arrays.equals(topicsBytes, SHIELDED_TRC20_LOG_TOPICS_NOTE_SPENT)) { + return 5; } } return 0; @@ -3909,7 +3942,9 @@ private DecryptNotesTRC20 queryTRC20NoteByIvk(long startNum, long endNum, int index = 0; for (TransactionInfo.Log log : logList) { int logType = getShieldedTRC20LogType(log, shieldedTRC20ContractAddress); - if (logType > 0) { + // Only note-producing log types (1..3) advance the note index; + // TokenBurn (4) and NoteSpent (5) do not emit a leaf. + if (logType > 0 && logType < 4) { noteBuilder = DecryptNotesTRC20.NoteTx.newBuilder(); noteBuilder.setTxid(ByteString.copyFrom(txId)); noteBuilder.setIndex(index); @@ -4001,7 +4036,8 @@ public DecryptNotesTRC20 scanShieldedTRC20NotesByIvk( private Optional getNoteTxFromLogListByOvk( DecryptNotesTRC20.NoteTx.Builder builder, - TransactionInfo.Log log, byte[] ovk, int logType) throws ZksnarkException { + TransactionInfo.Log log, byte[] ovk, int logType, byte[] pendingNf) + throws ZksnarkException { byte[] logData = log.getData().toByteArray(); if (!ArrayUtils.isEmpty(logData)) { if (logType > 0 && logType < 4) { @@ -4040,18 +4076,36 @@ private Optional getNoteTxFromLogListByOvk( } } } else if (logType == 4) { - //Data = toAddress(32) + value(32) + ciphertext(80) + padding(16) + // Data = toAddress(32) + value(32) + cipher(80) + nonce(12) + reserved/version(4) + if (logData.length < 64 + NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) { + return Optional.empty(); + } byte[] logToAddress = ByteArray.subArray(logData, 12, 32); byte[] logAmountArray = ByteArray.subArray(logData, 32, 64); byte[] cipher = ByteArray.subArray(logData, 64, 144); + byte[] nonceFromLog = ByteArray.subArray(logData, 144, + 144 + NoteEncryption.Encryption.BURN_NONCE_LEN); + byte[] reservedFromLog = ByteArray.subArray(logData, + 144 + NoteEncryption.Encryption.BURN_NONCE_LEN, + 144 + NoteEncryption.Encryption.BURN_NONCE_LEN + + NoteEncryption.Encryption.BURN_RESERVED_LEN); BigInteger logAmount = ByteUtil.bytesToBigInteger(logAmountArray); byte[] plaintext; byte[] amountArray = new byte[32]; byte[] decryptedAddress = new byte[20]; + + byte[] addr21FromLog = new byte[21]; + addr21FromLog[0] = Wallet.getAddressPreFixByte(); + System.arraycopy(logToAddress, 0, addr21FromLog, 1, 20); Optional decryptedText = NoteEncryption.Encryption - .decryptBurnMessageByOvk(ovk, cipher); + .decryptBurnMessageByOvk(ovk, cipher, nonceFromLog, reservedFromLog, pendingNf, + logAmountArray, addr21FromLog); + if (decryptedText.isPresent()) { plaintext = decryptedText.get(); + if (plaintext[32] != Wallet.getAddressPreFixByte()) { + return Optional.empty(); + } System.arraycopy(plaintext, 0, amountArray, 0, 32); System.arraycopy(plaintext, 33, decryptedAddress, 0, 20); BigInteger decryptedAmount = ByteUtil.bytesToBigInteger(amountArray); @@ -4091,15 +4145,24 @@ public DecryptNotesTRC20 scanShieldedTRC20NotesByOvk(long startNum, long endNum, if (!Objects.isNull(logList)) { Optional noteTx; int index = 0; + byte[] pendingNf = null; for (TransactionInfo.Log log : logList) { int logType = getShieldedTRC20LogType(log, shieldedTRC20ContractAddress); - if (logType > 0) { + if (logType == 5) { + byte[] logData = log.getData().toByteArray(); + if (logData.length >= 32) { + pendingNf = ByteArray.subArray(logData, 0, 32); + } + } else if (logType > 0) { noteBuilder = DecryptNotesTRC20.NoteTx.newBuilder(); noteBuilder.setTxid(ByteString.copyFrom(txid)); noteBuilder.setIndex(index); index += 1; - noteTx = getNoteTxFromLogListByOvk(noteBuilder, log, ovk, logType); + noteTx = getNoteTxFromLogListByOvk(noteBuilder, log, ovk, logType, pendingNf); noteTx.ifPresent(builder::addNoteTxs); + if (logType == 4) { + pendingNf = null; + } } } } @@ -4283,12 +4346,49 @@ public BytesMessage getTriggerInputForShieldedTRC20Contract( parameterType); if (parametersBuilder.getShieldedTRC20ParametersType() == ShieldedTRC20ParametersType.BURN) { byte[] burnCiper = ByteArray.fromHexString(shieldedTRC20Parameters.getTriggerContractInput()); - if (!ArrayUtils.isEmpty(burnCiper) && burnCiper.length == 80) { - parametersBuilder.setBurnCiphertext(burnCiper); - } else { + if (ArrayUtils.isEmpty(burnCiper) + || burnCiper.length != NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) { + if (!ArrayUtils.isEmpty(burnCiper) && burnCiper.length == 80) { + throw new ZksnarkException( + "legacy 80-byte burn cipher is deprecated and rejected; expected " + + NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE + "-byte burn record"); + } throw new ZksnarkException( "invalid shielded TRC-20 contract parameters for burn trigger input"); } + // v2-only: length alone would accept a legacy all-zero suffix and bypass + // the nf-bound nonce. Require reserved==v2 marker and nonce==derive(nf). + byte[] reserved = Arrays.copyOfRange(burnCiper, + NoteEncryption.Encryption.BURN_RESERVED_OFFSET, + NoteEncryption.Encryption.BURN_RESERVED_OFFSET + + NoteEncryption.Encryption.BURN_RESERVED_LEN); + if (!Arrays.equals(reserved, NoteEncryption.Encryption.getBurnRecordV2Marker())) { + throw new ZksnarkException( + "burn trigger input must be v2 (reserved=0x00000001); legacy/unknown markers rejected"); + } + if (shieldedTRC20Parameters.getSpendDescriptionList().size() != 1) { + throw new ZksnarkException( + "burn trigger input requires exactly one spendDescription for nf-bound nonce"); + } + byte[] nf = shieldedTRC20Parameters.getSpendDescription(0).getNullifier().toByteArray(); + if (nf.length != 32) { + throw new ZksnarkException( + "burn trigger input requires 32-byte spendDescription.nullifier"); + } + byte[] nonceFromInput = Arrays.copyOfRange(burnCiper, + NoteEncryption.Encryption.BURN_NONCE_OFFSET, + NoteEncryption.Encryption.BURN_NONCE_OFFSET + + NoteEncryption.Encryption.BURN_NONCE_LEN); + byte[] amount32 = ByteUtil.bigIntegerToBytes(value, 32); + byte[] addr21 = new byte[21]; + addr21[0] = Wallet.getAddressPreFixByte(); + System.arraycopy(transparentToAddressTvm, 0, addr21, 1, 20); + byte[] expectedNonce = NoteEncryption.Encryption.deriveBurnNonce(nf, amount32, addr21); + if (!Arrays.equals(nonceFromInput, expectedNonce)) { + throw new ZksnarkException( + "burn trigger input nonce does not match nonce bound to (nf, amount, addr)"); + } + parametersBuilder.setBurnCiphertext(burnCiper); } String input = parametersBuilder .getTriggerContractInput(shieldedTRC20Parameters, spendAuthoritySignature, value, false, diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 2d6660f9a6a..0bca242606e 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -80,8 +80,6 @@ public class Args extends CommonParameter { m.put("--storage-db-directory", "storage.db.directory"); m.put("--storage-db-engine", "storage.db.engine"); m.put("--storage-db-synchronous", "storage.db.sync"); - m.put("--storage-index-directory", "storage.index.directory"); - m.put("--storage-index-switch", "storage.index.switch"); m.put("--storage-transactionHistory-switch", "storage.transHistory.switch"); m.put("--contract-parse-enable", "event.subscribe.contractParse"); m.put("--support-constant", "vm.supportConstant"); @@ -167,16 +165,19 @@ public static void setParam(final String[] args, final String confFileName) { ? cmd.shellConfFileName : confFileName; Config config = Configuration.getByFileName(configFilePath); - // 2. Config overrides defaults + // 2. Config overrides defaults (event config bean is read here but not yet applied) applyConfigParams(config); - // 3. CLI overrides Config (highest priority) + // 3. CLI overrides Config (highest priority, including --es → eventSubscribe) applyCLIParams(cmd, jc); - // 4. Apply platform constraints (e.g. ARM64 forces RocksDB) + // 4. Apply event config after CLI + applyEventConfig(eventConfig); + + // 5. Apply platform constraints (e.g. ARM64 forces RocksDB) applyPlatformConstraints(); - // 5. Init witness (depends on CLI witness flag) + // 6. Init witness (depends on CLI witness flag) initLocalWitnesses(config, cmd); } @@ -212,12 +213,8 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setDbEngine(sc.getDb().getEngine()); PARAMETER.storage.setDbSync(sc.getDb().isSync()); PARAMETER.storage.setDbDirectory(sc.getDb().getDirectory()); - PARAMETER.storage.setIndexDirectory(sc.getIndex().getDirectory()); - String indexSwitch = sc.getIndex().getSwitch(); - PARAMETER.storage.setIndexSwitch( - org.apache.commons.lang3.StringUtils.isNotEmpty(indexSwitch) ? indexSwitch : "on"); PARAMETER.storage.setTransactionHistorySwitch(sc.getTransHistory().getSwitch()); - // contractParse is set in applyEventConfig — it belongs to event.subscribe domain + // contractParse is set in applyConfigParams alongside event config, not here PARAMETER.storage.setCheckpointVersion(sc.getCheckpoint().getVersion()); PARAMETER.storage.setCheckpointSync(sc.getCheckpoint().isSync()); @@ -325,6 +322,7 @@ private static void applyRateLimiterConfig(RateLimiterConfig rl) { PARAMETER.rateLimiterSyncBlockChain = rl.getP2p().getSyncBlockChain(); PARAMETER.rateLimiterFetchInvData = rl.getP2p().getFetchInvData(); PARAMETER.rateLimiterDisconnect = rl.getP2p().getDisconnect(); + PARAMETER.rateLimiterApiNonBlocking = rl.isApiNonBlocking(); // HTTP/RPC rate limiter items: convert bean lists to business objects RateLimiterInitialization initialization = new RateLimiterInitialization(); @@ -343,21 +341,21 @@ private static void applyRateLimiterConfig(RateLimiterConfig rl) { PARAMETER.rateLimiterInitialization = initialization; } + /** + * Package-private entry point only for tests + */ + static void applyEventConfig() { + applyEventConfig(eventConfig); + } + /** * Bridge EventConfig bean values to CommonParameter fields. * Converts EventConfig (raw bean) into EventPluginConfig and FilterQuery (business objects). */ private static void applyEventConfig(EventConfig ec) { - PARAMETER.eventSubscribe = ec.isEnable(); - // contractParse belongs to event.subscribe but Storage object holds it - PARAMETER.storage.setContractParseSwitch(ec.isContractParse()); - - // PARAMETER.eventPluginConfig and PARAMETER.eventFilter are only consumed by - // Manager.startEventSubscribing(), which itself is gated by isEventSubscribe() - // (= ec.isEnable()) at Manager.java:564. When subscribe is disabled, building - // these objects has no observable effect — skip both early so PARAMETER stays - // consistent with the runtime intent. - if (!ec.isEnable()) { + // cmd parameter has higher priority + PARAMETER.eventSubscribe = PARAMETER.eventSubscribe || ec.isEnable(); + if (!PARAMETER.eventSubscribe) { return; } @@ -463,8 +461,8 @@ private static void applyCommitteeConfig(CommitteeConfig cc) { PARAMETER.allowProtoFilterNum = cc.getAllowProtoFilterNum(); PARAMETER.allowAccountStateRoot = cc.getAllowAccountStateRoot(); PARAMETER.changedDelegation = cc.getChangedDelegation(); - PARAMETER.allowPBFT = cc.getAllowPBFT(); - PARAMETER.pBFTExpireNum = cc.getPBFTExpireNum(); + PARAMETER.allowPBFT = cc.getAllowPbft(); + PARAMETER.pBFTExpireNum = cc.getPbftExpireNum(); PARAMETER.allowTvmFreeze = cc.getAllowTvmFreeze(); PARAMETER.allowTvmVote = cc.getAllowTvmVote(); PARAMETER.allowTvmLondon = cc.getAllowTvmLondon(); @@ -545,8 +543,6 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.solidityHttpPort = http.getSolidityPort(); PARAMETER.pBFTHttpPort = http.getPBFTPort(); PARAMETER.httpMaxMessageSize = http.getMaxMessageSize(); - PARAMETER.maxNestingDepth = http.getMaxNestingDepth(); - PARAMETER.maxTokenCount = http.getMaxTokenCount(); // ---- JSON-RPC sub-bean ---- NodeConfig.JsonRpcConfig jsonrpc = nc.getJsonrpc(); @@ -579,14 +575,7 @@ private static void applyNodeConfig(NodeConfig nc) { // ---- Flat scalar fields ---- PARAMETER.nodeEffectiveCheckEnable = nc.isEffectiveCheckEnable(); - // fetchBlock.timeout — range check [100, 1000], default 500 - int fetchTimeout = nc.getFetchBlockTimeout(); - if (fetchTimeout > 1000) { - fetchTimeout = 1000; - } else if (fetchTimeout < 100) { - fetchTimeout = 100; - } - PARAMETER.fetchBlockTimeout = fetchTimeout; + PARAMETER.fetchBlockTimeout = nc.getFetchBlockTimeout(); PARAMETER.maxConnections = nc.getMaxConnections(); PARAMETER.minConnections = nc.getMinConnections(); @@ -606,14 +595,10 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.maxHttpConnectNumber = nc.getMaxHttpConnectNumber(); PARAMETER.netMaxTrxPerSecond = nc.getNetMaxTrxPerSecond(); - if (StringUtils.isEmpty(PARAMETER.trustNodeAddr)) { - String trustNode = nc.getTrustNode(); - PARAMETER.trustNodeAddr = StringUtils.isEmpty(trustNode) ? null : trustNode; - } + PARAMETER.trustNodeAddr = nc.getTrustNode(); PARAMETER.validateSignThreadNum = nc.getValidateSignThreadNum(); PARAMETER.walletExtensionApi = nc.isWalletExtensionApi(); - PARAMETER.receiveTcpMinDataLength = nc.getReceiveTcpMinDataLength(); PARAMETER.isOpenFullTcpDisconnect = nc.isOpenFullTcpDisconnect(); PARAMETER.nodeDetectEnable = nc.isNodeDetectEnable(); @@ -629,7 +614,7 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.shieldedTransInPendingMaxCounts = nc.getShieldedTransInPendingMaxCounts(); PARAMETER.agreeNodeCount = nc.getAgreeNodeCount(); - PARAMETER.setOpenHistoryQueryWhenLiteFN(nc.isOpenHistoryQueryWhenLiteFN()); + PARAMETER.openHistoryQueryWhenLiteFN = nc.isOpenHistoryQueryWhenLiteFN(); PARAMETER.nodeMetricsEnable = nc.isMetricsEnable(); PARAMETER.openPrintLog = nc.isOpenPrintLog(); PARAMETER.openTransactionSort = nc.isOpenTransactionSort(); @@ -770,9 +755,12 @@ public static void applyConfigParams( // node.shutdown — handled in applyNodeConfig - // Event config: bind from config.conf "event.subscribe" section + // Event config: read bean here; applyEventConfig() is called once in setParam() + // after applyCLIParams() so that --es is already reflected in eventSubscribe. eventConfig = EventConfig.fromConfig(config); - applyEventConfig(eventConfig); + // contractParse is event-domain but must be set from config before CLI can + // override it with --contract-parse-enable (which runs in applyCLIParams). + PARAMETER.storage.setContractParseSwitch(eventConfig.isContractParse()); logConfig(); } @@ -861,12 +849,6 @@ private static void applyCLIParams(CLIParameter cmd, JCommander jc) { if (assigned.contains("--contract-parse-enable")) { PARAMETER.storage.setContractParseSwitch(Boolean.valueOf(cmd.contractParseEnable)); } - if (assigned.contains("--storage-index-directory")) { - PARAMETER.storage.setIndexDirectory(cmd.storageIndexDirectory); - } - if (assigned.contains("--storage-index-switch")) { - PARAMETER.storage.setIndexSwitch(cmd.storageIndexSwitch); - } if (assigned.contains("--storage-transactionHistory-switch")) { PARAMETER.storage.setTransactionHistorySwitch(cmd.storageTransactionHistorySwitch); } diff --git a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java index 19a0e278e08..36f7ee4928d 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -52,14 +52,13 @@ public class HistoryBlockHashUtil { // Account template for the new-account branch of {@code deploy()} (no prior // state at the canonical address). Equivalent to create2's - // {@code createAccount(addr, name, Contract)}: only type, accountName, and - // address are set. The pre-existing-account branch never uses this template + // {@code createAccount(addr, Contract)}: only type, and address + // are set. The pre-existing-account branch never uses this template // — it mutates the existing capsule in place to preserve balance / asset // state, mirroring the CREATE2 collision path. Safe to share: the proto is // immutable, and AccountCapsule mutations rebuild via {@code toBuilder}. private static final Account HISTORY_STORAGE_ACCOUNT = Account.newBuilder() .setType(Protocol.AccountType.Contract) - .setAccountName(ByteString.copyFromUtf8(HISTORY_STORAGE_NAME)) .setAddress(ByteString.copyFrom(HISTORY_STORAGE_ADDRESS)) .build(); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index a534b9d1c5d..9d7a7c979b9 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1127,6 +1127,7 @@ private void switchFork(BlockCapsule newHead) .equals(binaryTree.getValue().peekLast().getParentHash())) { if (EventPluginLoader.getInstance().getVersion() == 0) { reOrgContractTrigger(); + reOrgBlockTrigger(); } reOrgLogsFilter(); eraseBlock(); @@ -1140,6 +1141,18 @@ private void switchFork(BlockCapsule newHead) Exception exception = null; // todo process the exception carefully later try (ISession tmpSession = revokingStore.buildSession()) { + if (!item.getBlk().validateSignature( + getDynamicPropertiesStore(), getAccountStore())) { + throw new ValidateSignatureException( + "switch fork: block " + item.getBlk().getNum() + " signature invalid"); + } + // The new branch is applied on a rewound, diverged state where account permissions + // may have changed, so a cached signature-verification result is no longer + // trustworthy. Clear it to force every transaction to re-validate its signature + // against the fork-chain state. + for (TransactionCapsule tx : item.getBlk().getTransactions()) { + tx.setVerified(false); + } applyBlock(item.getBlk().setSwitch(true)); tmpSession.commit(); } catch (AccountResourceInsufficientException @@ -1196,6 +1209,8 @@ private void switchFork(BlockCapsule newHead) } } } + // only reached when the whole new branch applied cleanly; a failed switch rethrows above + reApplyBlockEvents(first); } } @@ -1230,7 +1245,7 @@ public List getVerifyTxs(BlockCapsule block) { List txs = new ArrayList<>(); Map txMap = new HashMap<>(); - Set multiAddresses = new HashSet<>(); + Set multiAddresses = new HashSet<>(ownerAddressSet); pendingTransactions.forEach(capsule -> { String txId = Hex.toHexString(capsule.getTransactionId().getBytes()); @@ -1428,9 +1443,10 @@ void blockTrigger(final BlockCapsule block, long oldSolid, long newSolid) { return; } - // if event subscribe is enabled, post block trigger to queue - postBlockTrigger(block); + // if event subscribe is enabled, post block trigger to queue (real-time, not removed) + postBlockTrigger(block, false); // if event subscribe is enabled, post solidity trigger to queue + // (also emits solidified-mode block/transaction triggers) postSolidityTrigger(newSolid); } catch (Exception e) { logger.error("Block trigger failed. head: {}, oldSolid: {}, newSolid: {}", @@ -1529,6 +1545,9 @@ public TransactionInfo processTransaction(final TransactionCapsule trxCap, Block String.format(" %s transaction signature validate failed", txId)); } + if (!trxCap.isInBlock()) { + trxCap.sanitize(); + } TransactionTrace trace = new TransactionTrace(trxCap, StoreFactory.getInstance(), new RuntimeImpl()); trxCap.setTrxTrace(trace); @@ -1625,7 +1644,6 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { session.reset(); session.setValue(revokingStore.buildSession()); - HistoryBlockHashUtil.write(this, blockCapsule); accountStateCallBack.preExecute(blockCapsule); if (getDynamicPropertiesStore().getAllowMultiSign() == 1) { @@ -1638,6 +1656,8 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { } } + HistoryBlockHashUtil.write(this, blockCapsule); + Set accountSet = new HashSet<>(); AtomicInteger shieldedTransCounts = new AtomicInteger(0); List toBePacked = new ArrayList<>(); @@ -2194,6 +2214,27 @@ private void postSolidityFilter(final long oldSolidNum, final long latestSolidif } private void postSolidityTrigger(final long latestSolidifiedBlockNumber) { + // solidified-mode block trigger: emit the newly-solidified blocks (never removed, + // since solidified blocks cannot be reorged). + if (eventPluginLoaded && EventPluginLoader.getInstance().isBlockLogTriggerEnable() + && EventPluginLoader.getInstance().isBlockLogTriggerSolidified()) { + for (BlockCapsule capsule : getContinuousBlockCapsule(latestSolidifiedBlockNumber)) { + BlockLogTriggerCapsule blockLogTriggerCapsule = new BlockLogTriggerCapsule(capsule); + blockLogTriggerCapsule.setLatestSolidifiedBlockNumber(latestSolidifiedBlockNumber); + if (!triggerCapsuleQueue.offer(blockLogTriggerCapsule)) { + logger.info("Too many triggers, block trigger lost: {}.", capsule.getBlockId()); + } + } + } + + // solidified-mode transaction trigger: emit transactions of the newly-solidified blocks. + if (eventPluginLoaded && EventPluginLoader.getInstance().isTransactionLogTriggerEnable() + && EventPluginLoader.getInstance().isTransactionLogTriggerSolidified()) { + for (BlockCapsule capsule : getContinuousBlockCapsule(latestSolidifiedBlockNumber)) { + processTransactionTrigger(capsule, false); + } + } + if (eventPluginLoaded && EventPluginLoader.getInstance().isSolidityLogTriggerEnable()) { for (Long i : Args.getSolidityContractLogTriggerMap().keySet()) { postSolidityLogContractTrigger(i, latestSolidifiedBlockNumber); @@ -2222,7 +2263,7 @@ private void postSolidityTrigger(final long latestSolidifiedBlockNumber) { lastUsedSolidityNum = latestSolidifiedBlockNumber; } - private void processTransactionTrigger(BlockCapsule newBlock) { + private void processTransactionTrigger(BlockCapsule newBlock, boolean removed) { List transactionCapsuleList = newBlock.getTransactions(); // need to set eth compatible data from transactionInfoList @@ -2241,7 +2282,7 @@ private void processTransactionTrigger(BlockCapsule newBlock) { transactionCapsule.setBlockNum(newBlock.getNum()); cumulativeEnergyUsed += postTransactionTrigger(transactionCapsule, newBlock, i, - cumulativeEnergyUsed, cumulativeLogCount, transactionInfo, energyUnitPrice); + cumulativeEnergyUsed, cumulativeLogCount, transactionInfo, energyUnitPrice, removed); cumulativeLogCount += transactionInfo.getLogCount(); } @@ -2250,12 +2291,12 @@ private void processTransactionTrigger(BlockCapsule newBlock) { newBlock.getNum(), "the sizes of transactionInfoList and transactionCapsuleList are not equal"); for (TransactionCapsule e : newBlock.getTransactions()) { - postTransactionTrigger(e, newBlock); + postTransactionTrigger(e, newBlock, removed); } } } else { for (TransactionCapsule e : newBlock.getTransactions()) { - postTransactionTrigger(e, newBlock); + postTransactionTrigger(e, newBlock, removed); } } } @@ -2275,6 +2316,31 @@ private void reOrgLogsFilter() { } } + // Post the FULL-stream block and logs filters for each block of the new canonical branch + // (oldest-first). Must be kept in sync with the FULL-filter section of blockTrigger. + // Solidity filters are intentionally not posted here: solidification events for these + // blocks arrive later, when postSolidityFilter runs against the then-canonical chain. + // Re-emit the per-block subscription events for a newly-applied fork branch after a chain + // reorg: JSON-RPC block/logs filters and event-subscribe block/transaction triggers. The + // fork-switch path returns before blockTrigger() runs, so without this these forward events + // would be lost for the re-applied blocks (contract triggers are already re-emitted during + // applyBlock). All emitted as forward (removed=false): these blocks are now canonical. + private void reApplyBlockEvents(List newBranch) { + if (CommonParameter.getInstance().isJsonRpcHttpFullNodeEnable()) { + for (KhaosBlock khaosBlock : newBranch) { + BlockCapsule blockCapsule = khaosBlock.getBlk(); + postBlockFilter(blockCapsule, false); + postLogsFilter(blockCapsule, false, false); + } + } + + if (EventPluginLoader.getInstance().getVersion() == 0) { + for (KhaosBlock khaosBlock : newBranch) { + postBlockTrigger(khaosBlock.getBlk(), false); + } + } + } + private void postBlockFilter(final BlockCapsule blockCapsule, boolean solidified) { BlockFilterCapsule blockFilterCapsule = new BlockFilterCapsule(blockCapsule, solidified); @@ -2299,39 +2365,26 @@ private void postLogsFilter(final BlockCapsule blockCapsule, boolean solidified, } } - void postBlockTrigger(final BlockCapsule blockCapsule) { - // process block trigger + // Real-time block/transaction triggers for a single block. The solidified-mode batch is + // handled in postSolidityTrigger (driven by solidification advancement), so here we only + // emit for triggers configured as non-solidified. {@code removed=true} re-emits the same + // trigger when the block is rolled back by a chain reorg (see reOrgBlockTrigger). + void postBlockTrigger(final BlockCapsule blockCapsule, boolean removed) { long solidityBlkNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); - if (eventPluginLoaded && EventPluginLoader.getInstance().isBlockLogTriggerEnable()) { - List capsuleList = new ArrayList<>(); - if (EventPluginLoader.getInstance().isBlockLogTriggerSolidified()) { - capsuleList = getContinuousBlockCapsule(solidityBlkNum); - } else { - capsuleList.add(blockCapsule); - } - for (BlockCapsule capsule : capsuleList) { - BlockLogTriggerCapsule blockLogTriggerCapsule = new BlockLogTriggerCapsule(capsule); - blockLogTriggerCapsule.setLatestSolidifiedBlockNumber(solidityBlkNum); - if (!triggerCapsuleQueue.offer(blockLogTriggerCapsule)) { - logger.info("Too many triggers, block trigger lost: {}.", capsule.getBlockId()); - } + if (eventPluginLoaded && EventPluginLoader.getInstance().isBlockLogTriggerEnable() + && !EventPluginLoader.getInstance().isBlockLogTriggerSolidified()) { + BlockLogTriggerCapsule blockLogTriggerCapsule = new BlockLogTriggerCapsule(blockCapsule); + blockLogTriggerCapsule.setLatestSolidifiedBlockNumber(solidityBlkNum); + blockLogTriggerCapsule.setRemoved(removed); + if (!triggerCapsuleQueue.offer(blockLogTriggerCapsule)) { + logger.info("Too many triggers, block trigger lost: {}.", blockCapsule.getBlockId()); } } - // process transaction trigger - if (eventPluginLoaded && EventPluginLoader.getInstance().isTransactionLogTriggerEnable()) { - List capsuleList = new ArrayList<>(); - if (EventPluginLoader.getInstance().isTransactionLogTriggerSolidified()) { - capsuleList = getContinuousBlockCapsule(solidityBlkNum); - } else { - // need to reset block - capsuleList.add(blockCapsule); - } - - for (BlockCapsule capsule : capsuleList) { - processTransactionTrigger(capsule); - } + if (eventPluginLoaded && EventPluginLoader.getInstance().isTransactionLogTriggerEnable() + && !EventPluginLoader.getInstance().isTransactionLogTriggerSolidified()) { + processTransactionTrigger(blockCapsule, removed); } } @@ -2357,11 +2410,13 @@ private List getContinuousBlockCapsule(long solidityBlkNum) { // cumulativeEnergyUsed is the total of energy used before the current transaction private long postTransactionTrigger(final TransactionCapsule trxCap, final BlockCapsule blockCap, int index, long preCumulativeEnergyUsed, - long cumulativeLogCount, final TransactionInfo transactionInfo, long energyUnitPrice) { + long cumulativeLogCount, final TransactionInfo transactionInfo, long energyUnitPrice, + boolean removed) { TransactionLogTriggerCapsule trx = new TransactionLogTriggerCapsule(trxCap, blockCap, index, preCumulativeEnergyUsed, cumulativeLogCount, transactionInfo, energyUnitPrice); trx.setLatestSolidifiedBlockNumber(getDynamicPropertiesStore() .getLatestSolidifiedBlockNum()); + trx.setRemoved(removed); if (!triggerCapsuleQueue.offer(trx)) { logger.info("Too many triggers, transaction trigger lost: {}.", trxCap.getTransactionId()); } @@ -2371,10 +2426,11 @@ private long postTransactionTrigger(final TransactionCapsule trxCap, private void postTransactionTrigger(final TransactionCapsule trxCap, - final BlockCapsule blockCap) { + final BlockCapsule blockCap, boolean removed) { TransactionLogTriggerCapsule trx = new TransactionLogTriggerCapsule(trxCap, blockCap); trx.setLatestSolidifiedBlockNumber(getDynamicPropertiesStore() .getLatestSolidifiedBlockNum()); + trx.setRemoved(removed); if (!triggerCapsuleQueue.offer(trx)) { logger.info("Too many triggers, transaction trigger lost: {}.", trxCap.getTransactionId()); } @@ -2399,6 +2455,26 @@ private void reOrgContractTrigger() { clearSolidityContractTriggerCache(getHeadBlockNum()); } + // On a chain reorg, re-emit the block/transaction triggers of the block being erased with + // removed=true, so subscribers can roll back. Only real-time (non-solidified) triggers were + // ever emitted for this block, so postBlockTrigger(.., true) naturally no-ops in solidified + // mode. Called in the erase loop before eraseBlock(), so the old head is still current head. + private void reOrgBlockTrigger() { + if (eventPluginLoaded + && (EventPluginLoader.getInstance().isBlockLogTriggerEnable() + || EventPluginLoader.getInstance().isTransactionLogTriggerEnable())) { + logger.info("Switch fork occurred, post reOrgBlockTrigger."); + try { + BlockCapsule oldHeadBlock = chainBaseManager.getBlockById( + getDynamicPropertiesStore().getLatestBlockHeaderHash()); + postBlockTrigger(oldHeadBlock, true); + } catch (BadItemException | ItemNotFoundException e) { + logger.error("Block header hash does not exist or is bad: {}.", + getDynamicPropertiesStore().getLatestBlockHeaderHash()); + } + } + } + private void clearSolidityContractTriggerCache(long blockNum) { if (eventPluginLoaded && (EventPluginLoader.getInstance().isSolidityEventTriggerEnable() diff --git a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java index b9173b95cde..9dd950ae57b 100644 --- a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java +++ b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java @@ -215,7 +215,8 @@ private void processMessage(PeerConnection peer, byte[] data) { } } - private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) { + private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) + throws P2pException { InventoryType invType = msg.getInventoryType(); int currentSize = msg.getInventory().getIdsCount(); MessageStatistics stats = peer.getPeerStatistics().messageStatistics; @@ -237,6 +238,9 @@ private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) { peer.getInetAddress(), count, currentSize, maxBlockInvIn10s); return false; } + } else { + throw new P2pException(P2pException.TypeEnum.BAD_MESSAGE, + "unknown inventory type: " + msg.getInventory().getTypeValue()); } return true; } @@ -272,7 +276,8 @@ private void processException(PeerConnection peer, TronMessage msg, Exception ex code = Protocol.ReasonCode.BAD_TX; break; case BAD_BLOCK: - case BLOCK_SIGN_ERROR: + case BLOCK_SIGN_INVALID: + case BLOCK_MERKLE_INVALID: code = Protocol.ReasonCode.BAD_BLOCK; break; case NO_SUCH_MESSAGE: 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 804c3fffa39..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); @@ -312,7 +314,7 @@ public void processBlock(BlockCapsule block, boolean isSync) throws P2pException logger.error("Process block failed, {}, reason: {}", blockId.getString(), e.getMessage()); if (e instanceof BadBlockException && ((BadBlockException) e).getType().equals(CALC_MERKLE_ROOT_FAILED)) { - throw new P2pException(TypeEnum.BLOCK_MERKLE_ERROR, e); + throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e); } else { throw new P2pException(TypeEnum.BAD_BLOCK, e); } @@ -347,10 +349,10 @@ public void validSignature(BlockCapsule block) throws P2pException { flag = block.validateSignature(dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); } catch (Exception e) { - throw new P2pException(TypeEnum.BLOCK_SIGN_ERROR, e); + throw new P2pException(TypeEnum.BLOCK_SIGN_INVALID, e); } if (!flag) { - throw new P2pException(TypeEnum.BLOCK_SIGN_ERROR, "valid signature failed."); + throw new P2pException(TypeEnum.BLOCK_SIGN_INVALID, "valid signature failed."); } } @@ -363,7 +365,7 @@ public boolean validBlock(BlockCapsule block) throws P2pException { try { block.validateMerkleRoot(); } catch (BadBlockException e) { - throw new P2pException(TypeEnum.BLOCK_MERKLE_ERROR, e.getMessage()); + throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e.getMessage()); } validSignature(block); return witnessScheduleStore.getActiveWitnesses().contains(block.getWitnessAddress()); diff --git a/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java b/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java index d5aad2cd5c4..99be34e1bf1 100644 --- a/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java +++ b/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java @@ -28,6 +28,12 @@ public BlockMessage(BlockCapsule block) { this.block = block; } + public void sanitize() { + if (this.block.sanitize()) { + this.data = this.block.getData(); + } + } + public BlockId getBlockId() { return getBlockCapsule().getBlockId(); } diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java index 3b9e86d4791..452209d575f 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java @@ -77,6 +77,8 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep check(peer, blockMessage); } + blockMessage.sanitize(); + if (peer.getSyncBlockRequested().containsKey(blockId)) { peer.getSyncBlockRequested().remove(blockId); peer.getSyncBlockInProcess().add(blockId); diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java index b1f26468081..2d6d47d1dd1 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java @@ -160,9 +160,13 @@ private void check(PeerConnection peer, FetchInvDataMessage fetchInvDataMsg, "FetchInvData contains duplicate hashes, size: " + hashList.size()); } - MessageTypes type = fetchInvDataMsg.getInvMessageType(); + InventoryType invType = fetchInvDataMsg.getInventoryType(); + if (invType != InventoryType.TRX && invType != InventoryType.BLOCK) { + throw new P2pException(TypeEnum.BAD_MESSAGE, + "unknown inventory type: " + fetchInvDataMsg.getInventory().getTypeValue()); + } - if (type == MessageTypes.TRX) { + if (invType == InventoryType.TRX) { for (Sha256Hash hash : fetchInvDataMsg.getHashList()) { if (peer.getAdvInvSpread().getIfPresent(new Item(hash, InventoryType.TRX)) == null) { throw new P2pException(TypeEnum.BAD_MESSAGE, "not spread inv: " + hash); diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java index 59232a8d258..f96f7f0b0ff 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java @@ -63,6 +63,10 @@ private boolean check(PeerConnection peer, InventoryMessage inventoryMessage) } InventoryType type = inventoryMessage.getInventoryType(); + if (type != InventoryType.TRX && type != InventoryType.BLOCK) { + throw new P2pException(TypeEnum.BAD_MESSAGE, + "unknown inventory type: " + inventoryMessage.getInventory().getTypeValue()); + } int size = hashList.size(); if (peer.isNeedSyncFromPeer() || peer.isNeedSyncFromUs()) { diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java index e153e21f331..52137c5881c 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java @@ -1,5 +1,6 @@ package org.tron.core.net.messagehandler; +import com.google.protobuf.ByteString; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -13,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.crypto.SignUtils; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; @@ -142,6 +144,12 @@ private void check(PeerConnection peer, TransactionsMessage msg) throws P2pExcep throw new P2pException(TypeEnum.BAD_TRX, "tx " + item.getHash() + " contract size should be greater than 0"); } + for (ByteString sig : trx.getSignatureList()) { + if (!SignUtils.isValidLength(sig.size())) { + throw new P2pException(TypeEnum.BAD_TRX, + "tx " + item.getHash() + " signature size is " + sig.size()); + } + } } } diff --git a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java index 8d7818d1608..7d7457cf2fc 100644 --- a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java +++ b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java @@ -170,7 +170,8 @@ public class PeerConnection { public void setChannel(Channel channel) { this.channel = channel; - if (relayNodes.stream().anyMatch(n -> n.getAddress().equals(channel.getInetAddress()))) { + if (relayNodes != null + && relayNodes.stream().anyMatch(n -> n.getAddress().equals(channel.getInetAddress()))) { this.isRelayPeer = true; } this.nodeStatistics = TronStatsManager.getNodeStatistics(channel.getInetAddress()); diff --git a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java index 61ae6326e9f..d4e010ff21d 100644 --- a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java +++ b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java @@ -150,6 +150,12 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { return false; } + if (!SignUtils.isValidLength(msg.getSignature().size())) { + logger.warn("HelloMessage from {}, signature size is {}.", + channel.getInetAddress(), msg.getSignature().size()); + return false; + } + boolean flag; try { Sha256Hash hash = Sha256Hash.of(CommonParameter diff --git a/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java b/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java index 0ffe69db097..bd656d9c41e 100644 --- a/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java +++ b/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java @@ -342,8 +342,8 @@ private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection) } catch (P2pException p2pException) { logger.error("Process sync block {} failed, type: {}", blockId.getString(), p2pException.getType()); - attackFlag = p2pException.getType().equals(TypeEnum.BLOCK_SIGN_ERROR) - || p2pException.getType().equals(TypeEnum.BLOCK_MERKLE_ERROR); + attackFlag = p2pException.getType().equals(TypeEnum.BLOCK_SIGN_INVALID) + || p2pException.getType().equals(TypeEnum.BLOCK_MERKLE_INVALID); flag = false; } catch (Exception e) { logger.error("Process sync block {} failed", blockId.getString(), e); diff --git a/framework/src/main/java/org/tron/core/services/event/RealtimeEventService.java b/framework/src/main/java/org/tron/core/services/event/RealtimeEventService.java index 5aee55b1c13..cef16cd81c1 100644 --- a/framework/src/main/java/org/tron/core/services/event/RealtimeEventService.java +++ b/framework/src/main/java/org/tron/core/services/event/RealtimeEventService.java @@ -12,7 +12,6 @@ import org.tron.common.es.ExecutorServiceManager; import org.tron.common.logsfilter.EventPluginLoader; import org.tron.common.logsfilter.trigger.Trigger; -import org.tron.core.db.Manager; import org.tron.core.services.event.bo.BlockEvent; import org.tron.core.services.event.bo.Event; @@ -25,9 +24,6 @@ public class RealtimeEventService { @Getter private static Object contractLock = new Object(); - @Autowired - private Manager manager; - @Autowired private SolidEventService solidEventService; @@ -77,25 +73,31 @@ public synchronized void work() { public void flush(BlockEvent blockEvent, boolean isRemove) { logger.info("Flush realtime event {}", blockEvent.getBlockId().getString()); + // Post block/transaction triggers synchronously to the plugin (processTrigger -> + // EventPluginLoader serializes immediately) instead of the async triggerCapsuleQueue: the + // capsule is a shared cached object whose removed flag is set per-flush, so an async consumer + // could read it after a later flush overwrote it. This mirrors how contract triggers below + // are posted directly. isRemove=true re-emits the block/transaction as rolled back on a reorg. if (instance.isBlockLogTriggerEnable() - && !instance.isBlockLogTriggerSolidified() - && !isRemove) { + && !instance.isBlockLogTriggerSolidified()) { if (blockEvent.getBlockLogTriggerCapsule() == null) { logger.warn("BlockLogTriggerCapsule is null. {}", blockEvent.getBlockId().getString()); } else { - manager.getTriggerCapsuleQueue().offer(blockEvent.getBlockLogTriggerCapsule()); + blockEvent.getBlockLogTriggerCapsule().setRemoved(isRemove); + blockEvent.getBlockLogTriggerCapsule().processTrigger(); } } if (instance.isTransactionLogTriggerEnable() - && !instance.isTransactionLogTriggerSolidified() - && !isRemove) { + && !instance.isTransactionLogTriggerSolidified()) { if (blockEvent.getTransactionLogTriggerCapsules() == null) { logger.warn("TransactionLogTriggerCapsules is null. {}", blockEvent.getBlockId().getString()); } else { - blockEvent.getTransactionLogTriggerCapsules().forEach(v -> - manager.getTriggerCapsuleQueue().offer(v)); + blockEvent.getTransactionLogTriggerCapsules().forEach(v -> { + v.setRemoved(isRemove); + v.processTrigger(); + }); } } diff --git a/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java index 2ff8a5ad321..ed20630b780 100644 --- a/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/HttpInterceptor.java @@ -1,6 +1,5 @@ package org.tron.core.services.filter; -import com.google.common.base.Strings; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -9,6 +8,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; import org.tron.common.prometheus.MetricKeys; import org.tron.common.prometheus.MetricLabels; import org.tron.common.prometheus.Metrics; @@ -66,6 +67,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } MetricsUtil.meterMark(MetricsKey.NET_API_QPS, 1); MetricsUtil.meterMark(MetricsKey.NET_API_FAIL_QPS, 1); + if (e instanceof BadMessageException + && ((BadMessageException) e).getCode() == HttpStatus.PAYLOAD_TOO_LARGE_413) { + throw (BadMessageException) e; + } } } @@ -75,4 +80,3 @@ public void destroy() { } - diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 1dab6c7b941..e6ccb4e4d17 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -57,6 +57,7 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import org.tron.common.utils.ByteArray; import org.tron.common.utils.Commons; import org.tron.common.utils.StringUtil; +import org.tron.core.Constant; import org.tron.json.JSON; import org.tron.protos.contract.BalanceContract; @@ -291,6 +292,7 @@ public static void merge(CharSequence input, tokenizer.consume("{"); // Needs to happen when the object starts. while (!tokenizer.tryConsume("}")) { // Continue till the object is done mergeField(tokenizer, extensionRegistry, builder, selfType); + tokenizer.tryConsume(","); } // Test to make sure the tokenizer has reached the end of the stream. if (!tokenizer.atEnd()) { @@ -556,8 +558,9 @@ protected static StringBuilder toStringBuilder(Readable input) throws IOExceptio } /** - * Parse a single field from {@code tokenizer} and merge it into {@code builder}. If a ',' is - * detected after the field ends, the next field will be parsed automatically + * Parse a single field from {@code tokenizer} and merge it into {@code builder}. Exactly one + * field is consumed; the caller ({@code merge} / {@code handleObject}) consumes any trailing + * ',' and loops over the remaining fields. */ protected static void mergeField(Tokenizer tokenizer, ExtensionRegistry extensionRegistry, Message.Builder builder, @@ -617,6 +620,10 @@ protected static void mergeField(Tokenizer tokenizer, if (field != null) { tokenizer.consume(":"); + // Match protobuf JsonFormat: a field whose value is null is treated as absent. + if (tokenizer.tryConsume("null")) { + return; + } boolean array = tokenizer.tryConsume("["); if (array) { @@ -628,11 +635,6 @@ protected static void mergeField(Tokenizer tokenizer, handleValue(tokenizer, extensionRegistry, builder, field, extension, unknown, selfType); } } - - if (tokenizer.tryConsume(",")) { - // Continue with the next field - mergeField(tokenizer, extensionRegistry, builder, selfType); - } } private static void handleMissingField(Tokenizer tokenizer, @@ -642,18 +644,28 @@ private static void handleMissingField(Tokenizer tokenizer, if ("{".equals(tokenizer.currentToken())) { // Message structure tokenizer.consume("{"); - do { - tokenizer.consumeIdentifier(); - handleMissingField(tokenizer, extensionRegistry, builder); - } while (tokenizer.tryConsume(",")); - tokenizer.consume("}"); + tokenizer.enterRecursion(); + try { + do { + tokenizer.consumeIdentifier(); + handleMissingField(tokenizer, extensionRegistry, builder); + } while (tokenizer.tryConsume(",")); + tokenizer.consume("}"); + } finally { + tokenizer.exitRecursion(); + } } else if ("[".equals(tokenizer.currentToken())) { // Collection tokenizer.consume("["); - do { - handleMissingField(tokenizer, extensionRegistry, builder); - } while (tokenizer.tryConsume(",")); - tokenizer.consume("]"); + tokenizer.enterRecursion(); + try { + do { + handleMissingField(tokenizer, extensionRegistry, builder); + } while (tokenizer.tryConsume(",")); + tokenizer.consume("]"); + } finally { + tokenizer.exitRecursion(); + } } else { //if (!",".equals(tokenizer.currentToken)){ // Primitive value if ("null".equals(tokenizer.currentToken())) { @@ -807,20 +819,25 @@ private static Object handleObject(Tokenizer tokenizer, } tokenizer.consume("{"); - String endToken = "}"; + tokenizer.enterRecursion(); + try { + String endToken = "}"; - while (!tokenizer.tryConsume(endToken)) { - if (tokenizer.atEnd()) { - throw tokenizer.parseException("Expected \"" + endToken + "\"."); - } - mergeField(tokenizer, extensionRegistry, subBuilder, selfType); - if (tokenizer.tryConsume(",")) { - // there are more fields in the object, so continue - continue; + while (!tokenizer.tryConsume(endToken)) { + if (tokenizer.atEnd()) { + throw tokenizer.parseException("Expected \"" + endToken + "\"."); + } + mergeField(tokenizer, extensionRegistry, subBuilder, selfType); + if (tokenizer.tryConsume(",")) { + // there are more fields in the object, so continue + continue; + } } - } - return subBuilder.build(); + return subBuilder.build(); + } finally { + tokenizer.exitRecursion(); + } } /** @@ -1290,6 +1307,18 @@ protected static class Tokenizer { // errors *after* consuming). private int previousLine = 0; private int previousColumn = 0; + private int currentDepth = 0; + + public void enterRecursion() throws ParseException { + if (currentDepth >= Constant.MAX_NESTING_DEPTH) { + throw parseException("Hit recursion limit."); + } + ++currentDepth; + } + + public void exitRecursion() { + --currentDepth; + } /** * Construct a tokenizer that parses tokens from the given text. @@ -1313,7 +1342,18 @@ static ByteString unescapeBytesSelfType(String input, final String fliedName) throws InvalidEscapeSequence { //Address base58 -> ByteString if (HttpSelfFormatFieldName.isAddressFormat(fliedName)) { - return ByteString.copyFrom(Commons.decodeFromBase58Check(input)); + byte[] addressBytes = null; + try { + addressBytes = Commons.decodeFromBase58Check(input); + } catch (IllegalArgumentException e) { + // Base58.decode throws on illegal chars -> leave addressBytes null (treated as invalid) + } + if (addressBytes == null) { + // empty / wrong-length / bad-checksum / illegal chars -> all invalid addresses; throw a + // clear error instead of letting ByteString.copyFrom(null) throw a bare NPE. + throw new InvalidEscapeSequence("invalid address for field: " + fliedName); + } + return ByteString.copyFrom(addressBytes); } //Normal String -> ByteString diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 3086cbb3619..b5ae7d58623 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -14,6 +14,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.BadMessageException; import org.springframework.beans.factory.annotation.Autowired; import org.tron.common.parameter.RateLimiterInitialization; import org.tron.common.prometheus.MetricKeys; @@ -107,9 +108,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) IRateLimiter rateLimiter = container.get(KEY_PREFIX_HTTP, getClass().getSimpleName()); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) @@ -132,7 +134,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) resp.getWriter() .println(Util.printErrorMsg(new IllegalAccessException("lack of computing resources"))); } - } catch (ServletException | IOException e) { + } catch (ServletException | IOException | BadMessageException e) { throw e; } catch (Exception unexpected) { logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected); @@ -148,4 +150,4 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) } } } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index c4556e42c76..5be2495e1f7 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -210,9 +210,12 @@ public static String printTransactionSignWeight(TransactionSignWeight transactio String string = JsonFormat.printToString(transactionSignWeight, selfType); JSONObject jsonObject = JSONObject.parseObject(string); JSONObject jsonObjectExt = jsonObject.getJSONObject(TRANSACTION); - jsonObjectExt.put(TRANSACTION, - printTransactionToJSON(transactionSignWeight.getTransaction().getTransaction(), selfType)); - jsonObject.put(TRANSACTION, jsonObjectExt); + if (jsonObjectExt != null) { + jsonObjectExt.put(TRANSACTION, + printTransactionToJSON(transactionSignWeight.getTransaction().getTransaction(), + selfType)); + jsonObject.put(TRANSACTION, jsonObjectExt); + } return jsonObject.toJSONString(); } @@ -221,10 +224,12 @@ public static String printTransactionApprovedList(TransactionApprovedList transa String string = JsonFormat.printToString(transactionApprovedList, selfType); JSONObject jsonObject = JSONObject.parseObject(string); JSONObject jsonObjectExt = jsonObject.getJSONObject(TRANSACTION); - jsonObjectExt.put(TRANSACTION, - printTransactionToJSON(transactionApprovedList.getTransaction().getTransaction(), - selfType)); - jsonObject.put(TRANSACTION, jsonObjectExt); + if (jsonObjectExt != null) { + jsonObjectExt.put(TRANSACTION, + printTransactionToJSON(transactionApprovedList.getTransaction().getTransaction(), + selfType)); + jsonObject.put(TRANSACTION, jsonObjectExt); + } return jsonObject.toJSONString(); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 6a0957d62d2..f4bba9fbf37 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -8,11 +8,13 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.encoders.Hex; import org.tron.api.GrpcAPI.AssetIssueList; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; @@ -60,6 +62,7 @@ public class JsonRpcApiUtil { public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; public static final String BLOCK_NUM_ERROR = "invalid block number"; + public static final String TX_INDEX_ERROR = "invalid index value"; private static final SecureRandom random = new SecureRandom(); @@ -395,19 +398,24 @@ public static long getUnfreezeAssetAmount(byte[] addressBytes, Wallet wallet) { */ public static byte[] addressCompatibleToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { + // ADDRESS_SIZE (42) is the hex length of a 21-byte address; +2 leaves room for the optional + // "0x" prefix, so a 0x-prefixed 21-byte address (0x41..., 44 chars) is still accepted. + if (hexAddress == null || hexAddress.length() > DecodeUtil.ADDRESS_SIZE + 2) { + throw new JsonRpcInvalidParamsException("invalid address"); + } byte[] addressByte; try { addressByte = ByteArray.fromHexString(hexAddress); if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 && addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { - throw new JsonRpcInvalidParamsException("invalid address hash value"); + throw new JsonRpcInvalidParamsException("invalid address"); } if (addressByte.length == DecodeUtil.ADDRESS_SIZE / 2 - 1) { addressByte = ByteUtil.merge(new byte[] {DecodeUtil.addressPreFixByte}, addressByte); } else if (addressByte[0] != ByteArray.fromHexString(DecodeUtil.addressPreFixString)[0]) { // addressByte.length == DecodeUtil.ADDRESS_SIZE / 2 - throw new JsonRpcInvalidParamsException("invalid address hash value"); + throw new JsonRpcInvalidParamsException("invalid address"); } } catch (Exception e) { throw new JsonRpcInvalidParamsException(e.getMessage()); @@ -415,26 +423,65 @@ public static byte[] addressCompatibleToByteArray(String hexAddress) return addressByte; } + /** Matches a 32-byte hash hex string: optional 0x prefix + 64 hex chars (also caps length). */ + public static final String HASH_REGEX = "^(0x)?[0-9a-fA-F]{64}$"; + /** - * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. + * Convert a hash hex string (optional 0x prefix) to a byte array, validating + * format and length via {@link #HASH_REGEX} first. */ - public static byte[] addressToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { - byte[] addressByte = ByteArray.fromHexString(hexAddress); - if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { - throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + public static byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { + if (hash == null || !Pattern.matches(HASH_REGEX, hash)) { + throw new JsonRpcInvalidParamsException("invalid hash value"); } - return new DataWord(addressByte).getLast20Bytes(); + byte[] bHash; + try { + bHash = ByteArray.fromHexString(hash); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(e.getMessage()); + } + return bHash; } /** - * check if topic is hex string of size 64, padding 0 ahead if length is odd. + * Matches a 32-byte topic hex string: optional 0x prefix + 63 or 64 hex chars. + */ + public static final String TOPIC_REGEX = "^(0x)?[0-9a-fA-F]{63,64}$"; + + /** + * Convert a topic hex string (optional 0x prefix, leading zero may be omitted) to a 32-byte + * array, validating format and length via {@link #TOPIC_REGEX} first. */ public static byte[] topicToByteArray(String hexTopic) throws JsonRpcInvalidParamsException { - byte[] topicByte = ByteArray.fromHexString(hexTopic); - if (topicByte.length != 32) { + if (hexTopic == null || !Pattern.matches(TOPIC_REGEX, hexTopic)) { + throw new JsonRpcInvalidParamsException("invalid topic: " + hexTopic); + } + try { + return ByteArray.fromHexString(hexTopic); + } catch (Exception e) { throw new JsonRpcInvalidParamsException("invalid topic: " + hexTopic); } - return topicByte; + } + + /** + * convert 40 hex string of address to byte array, padding 0 ahead if length is odd. + */ + public static byte[] addressToByteArray(String hexAddress) throws JsonRpcInvalidParamsException { + if (hexAddress == null) { + throw new JsonRpcInvalidParamsException("address is null"); + } else if (hexAddress.length() > DecodeUtil.ADDRESS_SIZE) { + throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + } + byte[] addressByte; + try { + addressByte = ByteArray.fromHexString(hexAddress); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + } + if (addressByte.length != DecodeUtil.ADDRESS_SIZE / 2 - 1) { + throw new JsonRpcInvalidParamsException("invalid address: " + hexAddress); + } + return new DataWord(addressByte).getLast20Bytes(); } public static boolean paramStringIsNull(String string) { @@ -499,7 +546,10 @@ public static long parseQuantityValue(String value) throws JsonRpcInvalidParamsE throw new JsonRpcInvalidParamsException("invalid param value: invalid hex number"); } } - + // QUANTITY is unsigned; reject a signed ("0x-..") value instead of returning a negative. + if (callValue < 0) { + throw new JsonRpcInvalidParamsException("invalid param value: negative"); + } return callValue; } @@ -603,10 +653,11 @@ public static long parseBlockTag(String tag, Wallet wallet) /** * Max allowed length for a JSON-RPC block number hex/decimal input. * API-level DoS guard: rejects pathological inputs before BigInteger parsing, - * whose cost grows quadratically with length. Covers hex (0x + 64 chars for - * uint256) and decimal (78 chars for uint256) representations with headroom. + * whose cost grows quadratically with length. A block number fits a signed long, + * so the longest valid input is 19 chars (decimal Long.MAX_VALUE) or 18 (0x + 16 + * hex); 20 leaves a small margin. */ - private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + private static final int MAX_BLOCK_NUM_HEX_LEN = 20; /** * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, @@ -636,16 +687,50 @@ public static long parseBlockNumber(String blockNum) } /** - * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. - * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use - * isBlockTag/parseBlockTag and handle hex separately with hexToBigInteger. + * Parse a block tag, or a 0x-prefixed hex block number. */ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) throws JsonRpcInvalidParamsException { if (isBlockTag(blockNumOrTag)) { return parseBlockTag(blockNumOrTag, wallet); } - return ByteArray.jsonHexToLong(blockNumOrTag); + if (blockNumOrTag == null || !blockNumOrTag.startsWith("0x")) { + throw new JsonRpcInvalidParamsException("Incorrect hex syntax"); + } + return parseBlockNumber(blockNumOrTag); + } + + /** + * Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the + * longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix. + */ + private static final int MAX_TX_INDEX_HEX_LEN = 8; + + /** + * Parse a 0x-prefixed hex transaction index at the JSON-RPC boundary. + */ + public static int parseTxIndex(String index) throws JsonRpcInvalidParamsException { + if (index == null || index.length() > MAX_TX_INDEX_HEX_LEN + 2) { + throw new JsonRpcInvalidParamsException(TX_INDEX_ERROR); + } + try { + return ByteArray.jsonHexToInt(index); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(TX_INDEX_ERROR); + } + } + + /** + * Compute feeLimit = gas * energyFee with overflow protection. A gas value large enough to + * overflow a signed 64-bit feeLimit is rejected as invalid-params instead of silently wrapping + * to a bogus (possibly negative) value. + */ + public static long calcFeeLimit(long gas, long energyFee) throws JsonRpcInvalidParamsException { + try { + return StrictMathWrapper.multiplyExact(gas, energyFee); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException("invalid gas: fee limit overflow"); + } } public static String generateFilterId() { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 2093930ca98..ca249da4e5d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,6 +1,9 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -22,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; +import org.tron.core.Constant; import org.tron.core.services.filter.BufferedResponseWrapper; import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @@ -30,7 +34,17 @@ @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = buildMapper(); + + private static ObjectMapper buildMapper() { + JsonFactory factory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(Constant.MAX_NESTING_DEPTH) + .maxTokenCount(Constant.MAX_TOKEN_COUNT) + .build()) + .build(); + return new ObjectMapper(factory); + } private enum JsonRpcError { PARSE_ERROR(-32700), @@ -97,11 +111,20 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rootNode = MAPPER.readTree(body); if (rootNode == null || rootNode.isMissingNode()) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); return; } } catch (JsonProcessingException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + if (e instanceof StreamConstraintsException) { + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, e.getMessage(), null, false); + } else { + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); + } + return; + } + + if (!rootNode.isObject() && !rootNode.isArray()) { + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); return; } @@ -159,8 +182,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes JsonNode subRequest = rootNode.get(i); if (overflow) { - // Notifications (no "id") do not get a response even on overflow. - if (subRequest.has("id")) { + if (!subRequest.isObject()) { + batchResult.add(buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null)); + } else if (subRequest.has("id")) { + // Notifications (no "id") do not get a response even on overflow. batchResult.add(buildErrorNode(JsonRpcError.RESPONSE_TOO_LARGE, "Response exceeds the limit of " + maxResponseSize + " bytes", subRequest.get("id"))); @@ -168,6 +193,19 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes continue; } + if (!subRequest.isObject()) { + ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null); + byte[] errBytes = MAPPER.writeValueAsBytes(errNode); + int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0); + if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { + overflow = true; + } else { + accumulatedSize += addition; + } + batchResult.add(errNode); + continue; + } + byte[] subBody; try { subBody = MAPPER.writeValueAsBytes(subRequest); @@ -213,13 +251,14 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes // JSON-RPC 2.0 §6: MUST NOT return an empty Array when there are no response objects. if (batchResult.isEmpty()) { + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(0); return; } byte[] finalBytes = MAPPER.writeValueAsBytes(batchResult); - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(finalBytes.length); resp.getOutputStream().write(finalBytes); @@ -261,7 +300,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { bytes = MAPPER.writeValueAsBytes(errorObj); } - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index 4d919b81ece..6be47886117 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -3,15 +3,18 @@ import static org.tron.core.Wallet.CONTRACT_VALIDATE_ERROR; import static org.tron.core.services.http.Util.setTransactionExtraData; import static org.tron.core.services.http.Util.setTransactionPermissionId; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.BLOCK_NUM_ERROR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.FINALIZED_STR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.HASH_REGEX; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.calcFeeLimit; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.generateFilterId; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.hashToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseTxIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.google.common.annotations.VisibleForTesting; @@ -157,7 +160,11 @@ public enum RequestSource { private final Map blockFilter2ResultSolidity = new ConcurrentHashMap<>(); - public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; + // Storage key is a 32-byte word: 64 hex chars + optional "0x" prefix = 66 max. + // Reject oversized input before fromHexString / new DataWord, which would otherwise + // throw a RuntimeException whose message embeds the whole hex (amplifying log output) + // and surface as a -32603 Internal error instead of -32602 invalid params. + public static final int MAX_STORAGE_KEY_HEX_LEN = 66; public static final String INVALID_BLOCK_RANGE = "invalid block range params"; @@ -366,20 +373,6 @@ public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransac return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } - private byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException { - if (!Pattern.matches(HASH_REGEX, hash)) { - throw new JsonRpcInvalidParamsException("invalid hash value"); - } - - byte[] bHash; - try { - bHash = ByteArray.fromHexString(hash); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(e.getMessage()); - } - return bHash; - } - /** * Reject any block selector that is not "latest". * Accepts "latest" silently; throws for other tags, numeric blocks, or invalid input. @@ -612,8 +605,19 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT throws JsonRpcInvalidParamsException { requireLatestBlockTag(blockNumOrTag); + if (storageIdx == null || storageIdx.length() > MAX_STORAGE_KEY_HEX_LEN) { + throw new JsonRpcInvalidParamsException("invalid storage key value"); + } + byte[] addressByte = addressCompatibleToByteArray(address); + DataWord index; + try { + index = new DataWord(ByteArray.fromHexString(storageIdx)); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid storage key value"); + } + // get contract from contractStore BytesMessage.Builder build = BytesMessage.newBuilder(); BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); @@ -627,7 +631,7 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT storage.setContractVersion(smartContract.getVersion()); storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); - DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); + DataWord value = storage.getValue(index); return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } @@ -812,14 +816,9 @@ private TransactionResult formatTransactionResult(TransactionInfo transactioninf private TransactionResult getTransactionByBlockAndIndex(Block block, String index) throws JsonRpcInvalidParamsException { - int txIndex; - try { - txIndex = ByteArray.jsonHexToInt(index); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid index value"); - } + int txIndex = parseTxIndex(index); - if (txIndex >= block.getTransactionsCount()) { + if (txIndex < 0 || txIndex >= block.getTransactionsCount()) { return null; } @@ -934,9 +933,10 @@ public List getBlockReceipts(String blockNumOrHashOrTag) Block block = null; - if (Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { + if (blockNumOrHashOrTag != null && Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { block = getBlockByJsonHash(blockNumOrHashOrTag); } else { + // null falls through to getBlockByNumOrTag -> parseBlockNumber -> -32602 (not an NPE) block = getBlockByNumOrTag(blockNumOrHashOrTag); } @@ -1141,7 +1141,11 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, ABI.Builder abiBuilder = ABI.newBuilder(); if (StringUtils.isNotEmpty(args.getAbi())) { String abiStr = "{" + "\"entrys\":" + args.getAbi() + "}"; - JsonFormat.merge(abiStr, abiBuilder, args.isVisible()); + try { + JsonFormat.merge(abiStr, abiBuilder, args.isVisible()); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException("invalid abi"); + } } SmartContract.Builder smartBuilder = SmartContract.newBuilder(); @@ -1167,7 +1171,7 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, .createTransactionCapsule(build.build(), ContractType.CreateSmartContract).getInstance(); Transaction.Builder txBuilder = tx.toBuilder(); Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); - rawBuilder.setFeeLimit(args.parseGas() * wallet.getEnergyFee()); + rawBuilder.setFeeLimit(calcFeeLimit(args.parseGas(), wallet.getEnergyFee())); txBuilder.setRawData(rawBuilder); tx = setTransactionPermissionId(args.getPermissionId(), txBuilder.build()); @@ -1216,7 +1220,7 @@ private TransactionJson buildTriggerSmartContractTransaction(byte[] ownerAddress Transaction.Builder txBuilder = tx.toBuilder(); Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); - rawBuilder.setFeeLimit(args.parseGas() * wallet.getEnergyFee()); + rawBuilder.setFeeLimit(calcFeeLimit(args.parseGas(), wallet.getEnergyFee())); txBuilder.setRawData(rawBuilder); Transaction trx = wallet @@ -1544,7 +1548,8 @@ public LogFilterElement[] getLogs(FilterRequest fr) throws JsonRpcInvalidParamsE } @Override - public LogFilterElement[] getFilterLogs(String filterId) throws ExecutionException, + public LogFilterElement[] getFilterLogs(String filterId) throws + JsonRpcInvalidParamsException, ExecutionException, InterruptedException, BadItemException, ItemNotFoundException, JsonRpcMethodNotFoundException, JsonRpcTooManyResultException { disableInPBFT("eth_getFilterLogs"); @@ -1564,6 +1569,10 @@ public LogFilterElement[] getFilterLogs(String filterId) throws ExecutionExcepti LogFilterWrapper logFilterWrapper = eventFilter2Result.get(filterId).getLogFilterWrapper(); long currentMaxBlockNum = wallet.getNowBlock().getBlockHeader().getRawData().getNumber(); + // re-check the block range against the current head: the filter was created without the cap + // (eth_newFilter), so enforce it here to prevent an unbounded scan. + logFilterWrapper.validateBlockRange(currentMaxBlockNum); + return getLogsByLogFilterWrapper(logFilterWrapper, currentMaxBlockNum); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java index d2bd58f6c56..03232f3549d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilter.java @@ -57,6 +57,10 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { List addr = new ArrayList<>(); int i = 0; for (Object s : (ArrayList) fr.getAddress()) { + if (!(s instanceof String)) { + throw new JsonRpcInvalidParamsException( + String.format("invalid address at index %d: %s", i, s)); + } try { addr.add(addressToByteArray((String) s)); i++; @@ -93,6 +97,9 @@ public LogFilter(FilterRequest fr) throws JsonRpcInvalidParamsException { List t = new ArrayList<>(); for (Object s : ((ArrayList) topic)) { + if (!(s instanceof String)) { + throw new JsonRpcInvalidParamsException("invalid topic(s): " + s); + } try { t.add(new DataWord(topicToByteArray((String) s)).getData()); } catch (JsonRpcInvalidParamsException e) { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 0331ab3694a..0fdf174bb50 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -6,7 +6,6 @@ import com.google.protobuf.ByteString; import lombok.Getter; import org.apache.commons.lang3.StringUtils; -import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.config.args.Args; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; @@ -35,14 +34,14 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet long fromBlockSrc; long toBlockSrc; if (fr.getBlockHash() != null) { - String blockHash = ByteArray.fromHex(fr.getBlockHash()); if (fr.getFromBlock() != null || fr.getToBlock() != null) { throw new JsonRpcInvalidParamsException( "cannot specify both BlockHash and FromBlock/ToBlock, choose one or the other"); } + byte[] blockHashBytes = JsonRpcApiUtil.hashToByteArray(fr.getBlockHash()); Block block = null; if (wallet != null) { - block = wallet.getBlockById(ByteString.copyFrom(ByteArray.fromHexString(blockHash))); + block = wallet.getBlockById(ByteString.copyFrom(blockHashBytes)); } if (block == null) { throw new JsonRpcInvalidParamsException("invalid blockHash"); @@ -99,16 +98,23 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet throw new JsonRpcInvalidParamsException("please verify: fromBlock <= toBlock"); } } - - // till now, it needs to check block range for eth_getLogs - int maxBlockRange = Args.getInstance().getJsonRpcMaxBlockRange(); - if (checkBlockRange && maxBlockRange > 0 - && min(toBlockSrc, currentMaxBlockNum) - fromBlockSrc > maxBlockRange) { - throw new JsonRpcInvalidParamsException("exceed max block range: " + maxBlockRange); - } } this.fromBlock = fromBlockSrc; this.toBlock = toBlockSrc; + + // eth_getLogs enforces the block range at construction time. eth_newFilter creates the + // wrapper with checkBlockRange=false (no creation-time gate); eth_getFilterLogs re-runs this + // check against the current head before scanning so the cap cannot be bypassed. + if (checkBlockRange) { + validateBlockRange(currentMaxBlockNum); + } + } + + public void validateBlockRange(long currentMaxBlockNum) throws JsonRpcInvalidParamsException { + int maxBlockRange = Args.getInstance().getJsonRpcMaxBlockRange(); + if (maxBlockRange > 0 && min(toBlock, currentMaxBlockNum) - fromBlock > maxBlockRange) { + throw new JsonRpcInvalidParamsException("exceed max block range: " + maxBlockRange); + } } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java index 4b3043274d2..11c55e3a2c3 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java @@ -23,21 +23,43 @@ public class GlobalRateLimiter { public static boolean tryAcquire(RuntimeData runtimeData) { String ip = runtimeData.getRemoteAddr(); if (!Strings.isNullOrEmpty(ip)) { - RateLimiter r; - try { - // cache.get is atomic: only one loader executes per key under concurrent requests, - // preventing multiple RateLimiter instances from being created for the same IP. - r = cache.get(ip, () -> RateLimiter.create(IP_QPS)); - } catch (Exception e) { - logger.warn("Failed to load IP rate limiter for {}, denying request: {}", - ip, e.getMessage()); + RateLimiter r = loadIpLimiter(ip); + if (r == null || !r.tryAcquire()) { return false; } - if (!r.tryAcquire()) { + } + return rateLimiter.tryAcquire(); + } + + public static boolean acquire(RuntimeData runtimeData) { + String ip = runtimeData.getRemoteAddr(); + if (!Strings.isNullOrEmpty(ip)) { + RateLimiter r = loadIpLimiter(ip); + if (r == null) { return false; } + r.acquire(); + } + rateLimiter.acquire(); + return true; + } + + public static boolean acquirePermit(RuntimeData runtimeData) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(runtimeData) + : acquire(runtimeData); + } + + private static RateLimiter loadIpLimiter(String ip) { + try { + // cache.get is atomic: only one loader executes per key under concurrent requests, + // preventing multiple RateLimiter instances from being created for the same IP. + return cache.get(ip, () -> RateLimiter.create(IP_QPS)); + } catch (Exception e) { + logger.warn("Failed to load IP rate limiter for {}, denying request: {}", + ip, e.getMessage()); + return null; } - return rateLimiter.tryAcquire(); } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java index a07cf955828..85e94f2e768 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java @@ -108,9 +108,10 @@ public Listener interceptCall(ServerCall call, RuntimeData runtimeData = new RuntimeData(call); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); if (!acquireResource) { // Release the per-endpoint permit when global rejected, to avoid semaphore leak. diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java index 8f5b5a487bf..63d4cc77587 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java @@ -15,4 +15,9 @@ public DefaultBaseQqsAdapter(String paramString) { public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java index 4adc142ed28..eb85baa8b41 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java @@ -21,4 +21,8 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java index c6fb089063a..0ebd21149a7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(data.getRemoteAddr()); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(data.getRemoteAddr()); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java index 46ed8beee92..29f7b61b6a5 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java @@ -1,9 +1,17 @@ package org.tron.core.services.ratelimiter.adapter; +import org.tron.core.config.args.Args; import org.tron.core.services.ratelimiter.RuntimeData; public interface IRateLimiter { boolean tryAcquire(RuntimeData data); + boolean acquire(RuntimeData data); + + default boolean acquirePermit(RuntimeData data) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(data) + : acquire(data); + } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java index 846a5eb1c4e..62074eac885 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java index 0a29183d762..e7b7f560b29 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java @@ -3,11 +3,15 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class GlobalPreemptibleStrategy extends Strategy { public static final String STRATEGY_PARAM_PERMIT = "permit"; public static final int DEFAULT_PERMIT_NUM = 1; + public static final int DEFAULT_ACQUIRE_TIMEOUT = 2; private Semaphore sp; public GlobalPreemptibleStrategy(String paramString) { @@ -23,15 +27,25 @@ protected Map defaultParam() { return map; } - // Non-blocking: immediately rejects if no permit is available. - // Intentional change from the previous tryAcquire(2, TimeUnit.SECONDS) behaviour: - // blocking the caller for up to 2 s ties up Netty IO / gRPC executor threads and - // masks overload rather than shedding it. All rate-limiting in this stack is now - // non-blocking to keep the thread model consistent with GlobalRateLimiter. + // Non-blocking: immediately rejects if no permit is available. Used when the + // apiNonBlocking switch is on, to shed overload instead of tying up Netty IO / + // gRPC executor threads while waiting for a permit. public boolean tryAcquire() { return sp.tryAcquire(); } + public boolean acquire() { + try { + return sp.tryAcquire(DEFAULT_ACQUIRE_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Restore the interrupt flag and reject — caller must not release a permit + // it never acquired. + logger.error("acquire permit with error: {}", e.getMessage()); + Thread.currentThread().interrupt(); + return false; + } + } + public void release() { sp.release(); } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java index 6589c90fe1d..7ffd1f04eb7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java @@ -22,17 +22,29 @@ public IPQpsStrategy(String paramString) { } public boolean tryAcquire(String ip) { - RateLimiter limiter; + RateLimiter limiter = loadLimiter(ip); + return limiter != null && limiter.tryAcquire(); + } + + public boolean acquire(String ip) { + RateLimiter limiter = loadLimiter(ip); + if (limiter == null) { + return false; + } + limiter.acquire(); + return true; + } + + private RateLimiter loadLimiter(String ip) { try { // cache.get is atomic: only one loader executes per key under concurrent requests, // preventing multiple RateLimiter instances from being created for the same IP. - limiter = ipLimiter.get(ip, this::newRateLimiter); + return ipLimiter.get(ip, this::newRateLimiter); } catch (Exception e) { logger.warn("Failed to load IP rate limiter for {}, denying request: {}", ip, e.getMessage()); - return false; + return null; } - return limiter.tryAcquire(); } private RateLimiter newRateLimiter() { diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java index 7e0466448b3..9116af1b7da 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java @@ -29,4 +29,9 @@ protected Map defaultParam() { public boolean tryAcquire() { return rateLimiter.tryAcquire(); } + + public boolean acquire() { + rateLimiter.acquire(); + return true; + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java index 4b980c7b7c9..4ee4f75a171 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -30,6 +30,7 @@ import org.tron.core.zen.address.PaymentAddress; import org.tron.core.zen.note.Note; import org.tron.core.zen.note.NoteEncryption; +import org.tron.core.zen.note.NoteEncryption.Encryption; import org.tron.core.zen.note.OutgoingPlaintext; import org.tron.protos.contract.ShieldContract; import org.tron.protos.contract.ShieldContract.ReceiveDescription; @@ -61,7 +62,17 @@ public class ShieldedTRC20ParametersBuilder { @Setter private BigInteger transparentToAmount; @Setter - private byte[] burnCiphertext = new byte[80]; + private byte[] ovk; + private byte[] burnCiphertext = new byte[Encryption.BURN_CIPHER_RECORD_SIZE]; + + public void setBurnCiphertext(byte[] burnCiphertext) { + if (burnCiphertext == null + || burnCiphertext.length != Encryption.BURN_CIPHER_RECORD_SIZE) { + throw new IllegalArgumentException( + "burnCiphertext must be " + Encryption.BURN_CIPHER_RECORD_SIZE + " bytes"); + } + this.burnCiphertext = burnCiphertext.clone(); + } public ShieldedTRC20ParametersBuilder() { @@ -207,6 +218,9 @@ private ReceiveDescriptionCapsule generateOutputProof(ReceiveDescriptionInfo out private void createSpendAuth(byte[] dataToBeSigned) throws ZksnarkException { for (int i = 0; i < spends.size(); i++) { + if (spends.get(i).expsk == null) { + throw new ZksnarkException("missing expanded spending key for spend authorization"); + } byte[] result = new byte[64]; JLibrustzcash.librustzcashSaplingSpendSig( new LibrustzcashParam.SpendSigParams(spends.get(i).expsk.getAsk(), @@ -292,6 +306,25 @@ public ShieldedTRC20Parameters build(boolean withAsk) throws ZksnarkException { SpendDescriptionInfo spend = spends.get(0); spendDescription = generateSpendProof(spend, ctx).getInstance(); builder.addSpendDescription(spendDescription); + + if (ovk == null && spend.expsk != null) { + ovk = spend.expsk.getOvk(); + } + if (ovk == null) { + throw new ZksnarkException("missing ovk for burn encryption"); + } + byte[] nf = spendDescription.getNullifier().toByteArray(); + byte[] transparentToAddressTvm = normalizeTransparentToAddress(transparentToAddress); + byte[] addr21 = new byte[21]; + addr21[0] = Wallet.getAddressPreFixByte(); + System.arraycopy(transparentToAddressTvm, 0, addr21, 1, 20); + Optional cipherOpt = Encryption.encryptBurnMessageByOvk( + ovk, transparentToAmount, addr21, nf); + if (!cipherOpt.isPresent()) { + throw new ZksnarkException("encrypt burn message failed"); + } + burnCiphertext = cipherOpt.get(); + mergedBytes = ByteUtil.merge(shieldedTRC20Address, encodeSpendDescriptionWithoutSpendAuthSig(spendDescription)); if (receives.size() == 1) { @@ -302,7 +335,7 @@ public ShieldedTRC20Parameters build(boolean withAsk) throws ZksnarkException { encodeCencCout(receiveDescription)); } mergedBytes = ByteUtil - .merge(mergedBytes, transparentToAddress, ByteArray.fromLong(valueBalance)); + .merge(mergedBytes, transparentToAddressTvm, ByteArray.fromLong(valueBalance)); value = transparentToAmount; builder.setParameterType("burn"); break; @@ -476,12 +509,10 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, throw new IllegalArgumentException("the value must be positive"); } - if (ArrayUtils.isEmpty(transparentToAddress)) { - throw new IllegalArgumentException("the transparent payTo address is null"); - } + byte[] transparentToAddressTvm = normalizeTransparentToAddress(transparentToAddress); payTo[11] = Wallet.getAddressPreFixByte(); - System.arraycopy(transparentToAddress, 0, payTo, 12, 20); + System.arraycopy(transparentToAddressTvm, 0, payTo, 12, 20); ShieldContract.SpendDescription spendDesc = burnParams.getSpendDescription(0); byte[] spendAuthSign; @@ -492,7 +523,6 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, } byte[] mergedBytes; - byte[] zeros = new byte[16]; mergedBytes = ByteUtil.merge( spendDesc.getNullifier().toByteArray(), spendDesc.getAnchor().toByteArray(), @@ -503,8 +533,7 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, ByteUtil.bigIntegerToBytes(value, 32), burnParams.getBindingSignature().toByteArray(), payTo, - burnCiphertext, - zeros + burnCiphertext ); byte[] outputOffsetBytes; // 32 @@ -524,7 +553,7 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, coffsetBytes = ByteUtil.longTo32Bytes(mergedBytes.length + 32 * 3 + 32L * 9); countBytes = ByteUtil.longTo32Bytes(1L); ReceiveDescription recvDesc = burnParams.getReceiveDescription(0); - zeros = new byte[12]; + byte[] zeros = new byte[12]; mergedBytes = ByteUtil .merge(mergedBytes, outputOffsetBytes, @@ -542,6 +571,18 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, return Hex.toHexString(mergedBytes); } + private byte[] normalizeTransparentToAddress(byte[] transparentToAddress) { + if (transparentToAddress != null && transparentToAddress.length == 20) { + return transparentToAddress; + } + if (transparentToAddress != null && transparentToAddress.length == 21) { + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + return transparentToAddressTvm; + } + throw new IllegalArgumentException("invalid transparentToAddress for burn encryption"); + } + public void addSpend( ExpandedSpendingKey expsk, Note note, diff --git a/framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java b/framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java index 7d9de4ff596..048f90dd9d2 100644 --- a/framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java +++ b/framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java @@ -8,10 +8,13 @@ import static org.tron.core.zen.note.NoteEncryption.Encryption.NOTEENCRYPTION_CIPHER_KEYSIZE; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import org.tron.common.crypto.Hash; import org.tron.common.utils.ByteUtil; import org.tron.common.zksnark.JLibrustzcash; import org.tron.common.zksnark.JLibsodium; @@ -111,6 +114,19 @@ public OutCiphertext encryptToOurselves( public static class Encryption { public static final int NOTEENCRYPTION_CIPHER_KEYSIZE = 32; + public static final int BURN_CIPHER_LEN = 80; + public static final int BURN_NONCE_LEN = 12; + public static final int BURN_RESERVED_LEN = 4; + public static final int BURN_CIPHER_RECORD_SIZE = 96; + public static final int BURN_NONCE_OFFSET = BURN_CIPHER_LEN; + public static final int BURN_RESERVED_OFFSET = BURN_NONCE_OFFSET + BURN_NONCE_LEN; + private static final byte[] BURN_RECORD_V2_MARKER = new byte[]{0, 0, 0, 1}; + private static final byte[] BURN_NONCE_DOMAIN = + "Ztron_BurnNonce".getBytes(StandardCharsets.UTF_8); + + public static byte[] getBurnRecordV2Marker() { + return BURN_RECORD_V2_MARKER.clone(); + } /** * generate ock by ovk, cv, cm, epk @@ -246,47 +262,137 @@ public static Optional attemptOutDecryption( } /** - * encrypt the message by ovk used for scanning + * encrypt burn message with nonce bound to (nf, amount, addr21), returns a 96B record: + * cipher(80) + nonce(12) + reserved/version(4). */ public static Optional encryptBurnMessageByOvk(byte[] ovk, BigInteger toAmount, - byte[] transparentToAddress) + byte[] transparentToAddress, byte[] nf) throws ZksnarkException { + if (ovk == null || ovk.length != NOTEENCRYPTION_CIPHER_KEYSIZE) { + throw new ZksnarkException("invalid ovk length"); + } + if (transparentToAddress == null || transparentToAddress.length != 21) { + throw new ZksnarkException("invalid transparentToAddress length"); + } + if (nf == null || nf.length != 32) { + throw new ZksnarkException("invalid nullifier length"); + } byte[] plaintext = new byte[64]; byte[] amountArray = ByteUtil.bigIntegerToBytes(toAmount, 32); - byte[] cipherNonce = new byte[12]; - byte[] cipher = new byte[80]; + byte[] nonce = deriveBurnNonce(nf, amountArray, transparentToAddress); + byte[] cipher = new byte[BURN_CIPHER_LEN]; System.arraycopy(amountArray, 0, plaintext, 0, 32); - System.arraycopy(transparentToAddress, 0, plaintext, 32, - 21); + System.arraycopy(transparentToAddress, 0, plaintext, 32, 21); if (JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt(new Chacha20Poly1305IetfEncryptParams( cipher, null, plaintext, - 64, null, 0, null, cipherNonce, ovk)) != 0) { + 64, null, 0, null, nonce, ovk)) != 0) { return Optional.empty(); } - return Optional.of(cipher); + byte[] record = new byte[BURN_CIPHER_RECORD_SIZE]; + System.arraycopy(cipher, 0, record, 0, BURN_CIPHER_LEN); + System.arraycopy(nonce, 0, record, BURN_NONCE_OFFSET, BURN_NONCE_LEN); + System.arraycopy(BURN_RECORD_V2_MARKER, 0, record, BURN_RESERVED_OFFSET, BURN_RESERVED_LEN); + return Optional.of(record); + } + + /** + * Derive a 12-byte ChaCha20-Poly1305 nonce from (nf, amount, addr21). + * Binding the plaintext fields ensures that repeated encryption with the same nf + * but different amount/addr produces distinct nonces, preserving AEAD nonce + * uniqueness even when the same input note is used to generate multiple burn + * trigger inputs off-chain. + */ + public static byte[] deriveBurnNonce(byte[] nf, byte[] amount32, byte[] addr21) { + if (nf == null || nf.length != 32) { + throw new IllegalArgumentException("invalid nullifier length"); + } + if (amount32 == null || amount32.length != 32) { + throw new IllegalArgumentException("invalid amount length"); + } + if (addr21 == null || addr21.length != 21) { + throw new IllegalArgumentException("invalid addr21 length"); + } + byte[] tagged = new byte[BURN_NONCE_DOMAIN.length + nf.length + amount32.length + + addr21.length]; + int off = 0; + System.arraycopy(BURN_NONCE_DOMAIN, 0, tagged, off, BURN_NONCE_DOMAIN.length); + off += BURN_NONCE_DOMAIN.length; + System.arraycopy(nf, 0, tagged, off, nf.length); + off += nf.length; + System.arraycopy(amount32, 0, tagged, off, amount32.length); + off += amount32.length; + System.arraycopy(addr21, 0, tagged, off, addr21.length); + byte[] hash = Hash.sha3(tagged); + byte[] nonce = new byte[BURN_NONCE_LEN]; + System.arraycopy(hash, 0, nonce, 0, BURN_NONCE_LEN); + return nonce; } /** - * decrypt the message by ovk used for scanning + * decrypt burn message. The trailing 4-byte reserved field is treated as an explicit + * record-version marker: + * - reserved = 0x00000000 and nonce = 0x000000000000000000000000 -> legacy v1 path. + * - reserved = 0x00000001 -> v2 path; nonce must equal + * deriveBurnNonce(nf, amount32, addr21) using the public log fields. + * - any other reserved value -> reject. */ - public static Optional decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext) + public static Optional decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext, + byte[] nonceFromLog, byte[] reservedFromLog, byte[] nf, byte[] amount32, byte[] addr21) throws ZksnarkException { + if (ovk == null || ovk.length != NOTEENCRYPTION_CIPHER_KEYSIZE) { + throw new ZksnarkException("invalid ovk length"); + } + if (ciphertext == null || ciphertext.length != BURN_CIPHER_LEN + || nonceFromLog == null || nonceFromLog.length != BURN_NONCE_LEN + || reservedFromLog == null || reservedFromLog.length != BURN_RESERVED_LEN) { + return Optional.empty(); + } + + byte[] effectiveNonce; + if (isAllZero(reservedFromLog)) { + if (!isAllZero(nonceFromLog)) { + return Optional.empty(); + } + effectiveNonce = nonceFromLog; + } else if (Arrays.equals(reservedFromLog, BURN_RECORD_V2_MARKER)) { + if (nf == null || nf.length != 32 + || amount32 == null || amount32.length != 32 + || addr21 == null || addr21.length != 21) { + return Optional.empty(); + } + byte[] derived = deriveBurnNonce(nf, amount32, addr21); + if (!Arrays.equals(nonceFromLog, derived)) { + return Optional.empty(); + } + effectiveNonce = nonceFromLog; + } else { + return Optional.empty(); + } + byte[] outPlaintext = new byte[64]; - byte[] cipherNonce = new byte[12]; if (JLibsodium.cryptoAeadChacha20poly1305IetfDecrypt(new Chacha20poly1305IetfDecryptParams( outPlaintext, null, null, - ciphertext, 80, + ciphertext, BURN_CIPHER_LEN, null, 0, - cipherNonce, ovk)) != 0) { + effectiveNonce, ovk)) != 0) { return Optional.empty(); } return Optional.of(outPlaintext); } + private static boolean isAllZero(byte[] data) { + for (byte b : data) { + if (b != 0) { + return false; + } + } + return true; + } + public static class EncCiphertext { @Getter diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 308cb9a1c69..96b9f73d577 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -1,8 +1,8 @@ package org.tron.program; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.util.ObjectUtils; import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; @@ -35,7 +35,7 @@ public static void main(String[] args) { } if (parameter.isSolidityNode()) { logger.info("Solidity node is running."); - if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) { + if (StringUtils.isEmpty(parameter.getTrustNodeAddr())) { throw new TronError(new IllegalArgumentException("Trust node is not set."), TronError.ErrCode.SOLID_NODE_INIT); } diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java index 0998d8846c0..beb9ede2e14 100644 --- a/framework/src/main/java/org/tron/program/SolidityNode.java +++ b/framework/src/main/java/org/tron/program/SolidityNode.java @@ -117,7 +117,15 @@ private void getBlock() { Block block = getBlockByNum(blockNum); blockQueue.put(block); blockNum = ID.incrementAndGet(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.info("getBlock interrupted, exiting."); + return; } catch (Exception e) { + if (!flag || tronNetDelegate.isHitDown()) { + logger.info("getBlock stopped during shutdown, last block: {}.", blockNum); + return; + } logger.error("Failed to get block {}, reason: {}.", blockNum, e.getMessage()); sleep(exceptionSleepTime); } @@ -177,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); } @@ -194,6 +206,10 @@ private long getLastSolidityBlockNum() { blockNum, remoteBlockNum, System.currentTimeMillis() - time); return blockNum; } catch (Exception e) { + if (!flag || tronNetDelegate.isHitDown()) { + logger.info("getLastSolidityBlockNum stopped during shutdown."); + return 0; + } logger.error("Failed to get last solid blockNum: {}, reason: {}.", remoteBlockNum.get(), e.getMessage()); sleep(exceptionSleepTime); @@ -205,8 +221,8 @@ private long getLastSolidityBlockNum() { public void sleep(long time) { try { Thread.sleep(time); - } catch (Exception e1) { - logger.error(e1.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index 3ce7ce20312..bf435bf0956 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.1-173-gaced0d5654"; + public static final String VERSION_CODE = "18817"; private static final String VERSION = "4.8.2"; public static String getVersion() { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index d00f334f4ce..1176dd46311 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -4,7 +4,6 @@ net { } storage { - # Directory for storing persistent data db.engine = "LEVELDB", // deprecated for arm, because arm only support "ROCKSDB". db.sync = false, db.directory = "database", @@ -12,57 +11,18 @@ storage { # Whether to write transaction result in transactionRetStore transHistory.switch = "on", - # setting can improve leveldb performance .... start, deprecated for arm - # node: if this will increase process fds,you may be 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 - # default = { - # maxOpenFiles = 100 - # } - # defaultM = { - # maxOpenFiles = 500 - # } - # defaultL = { - # maxOpenFiles = 1000 - # } - # setting can improve leveldb performance .... end, deprecated for arm - - # You can customize the configuration for each database. Otherwise, the database settings will use - # their defaults, and data will be stored in the "output-directory" or in the directory specified - # by the "-d" or "--output-directory" option. Attention: name is a required field that must be set! - # In this configuration, the name and path properties take effect for both LevelDB and RocksDB storage engines, - # while the additional properties (such as createIfMissing, paranoidChecks, compressionType, etc.) only take effect when using LevelDB. + # Per-database storage path overrides (name is required; see reference.conf for full property list). 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 - # }, - # { - # name = "account-index", - # path = "storage_directory_test", - # createIfMissing = true, - # 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 - # }, + # { + # name = "account", + # path = "storage_directory_test", + # maxOpenFiles = 100 + # }, ] needToUpdateAsset = true - # dbsettings is needed when using rocksdb as the storage implement (db.engine="ROCKSDB"). - # we'd strongly recommend that do not modify it unless you know every item's meaning clearly. + # RocksDB settings (only used when db.engine = "ROCKSDB"). See reference.conf for details. dbSettings = { levelNumber = 7 # compactThreads = 32 @@ -77,24 +37,8 @@ storage { balance.history.lookup = false - # checkpoint.version = 2 - # checkpoint.sync = true - - # the estimated number of block transactions (default 1000, min 100, max 10000). - # so the total number of cached transactions is 65536 * txCache.estimatedTransactions - # txCache.estimatedTransactions = 1000 - - # if true, transaction cache initialization will be faster. Default: false + # If true, transaction cache initialization will be faster. Default: false txCache.initOptimization = true - - # The number of blocks flushed to db in each batch during node syncing. Default: 1 - # snapshot.maxFlushCount = 1 - - # data root setting, for check data, currently, only reward-vi is used. - # merkleRoot = { - # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net, Sha256Hash, hexString - # } - } node.discovery = { @@ -102,24 +46,17 @@ node.discovery = { persist = true } -# custom stop condition -#node.shutdown = { -# BlockTime = "54 59 08 * * ?" # if block header time in persistent db matched. -# BlockHeight = 33350800 # if block header height in persistent db matched. -# BlockCount = 12 # block sync count after node start. -#} +# Custom stop condition +# node.shutdown = { +# BlockTime = "54 59 08 * * ?" # if block header time in persistent db matched. +# BlockHeight = 33350800 # if block header height in persistent db matched. +# BlockCount = 12 # block sync count after node start. +# } node.backup { - # udp listen port, each member should have the same configuration port = 10001 - - # my priority, each member should use different priority priority = 8 - - # time interval to send keepAlive message, each member should have the same configuration keepAliveInterval = 3000 - - # peer's ip list, can't contain mine members = [ # "ip", # "ip" @@ -132,17 +69,13 @@ crypto { } node.metrics = { - # prometheus metrics prometheus { enable = false port = 9527 } - } node { - # trust node for solidity node - # trustNode = "ip:port" trustNode = "127.0.0.1:50051" # expose extension api to public or not @@ -151,50 +84,14 @@ node { listen.port = 18888 fetchBlock.timeout = 200 - # syncFetchBatchNum = 2000 - - # Maximum number of blocks allowed in-flight (requested but not yet processed). - # Throttles block download to reduce memory pressure during sync. - # Range: [50, 2000], default: 500 - # maxPendingBlockSize = 500 - - # Maximum total number of cached transactions (handler queues + pending + rePush). - # When exceeded, the node stops accepting TRX INV messages from peers. - # maxTrxCacheSize = 50000 - - # Number of validate sign thread, default availableProcessors - # validateSignThreadNum = 16 maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - maxConnectionsWithSameIp = 2 - maxHttpConnectNumber = 50 - minParticipationRate = 15 - # WARNING: Some shielded transaction APIs require sending private keys as parameters. - # Calling these APIs on untrusted or remote nodes may leak your private keys. - # It is recommended to invoke them locally for development and testing. - # To opt in, set: allowShieldedTransactionApi = true - # Migration: the legacy key node.fullNodeAllowShieldedTransaction is still supported - # but deprecated; please migrate to node.allowShieldedTransactionApi. - # allowShieldedTransactionApi = false - - # openPrintLog = true - - # If set to true, SR packs transactions into a block in descending order of fee; otherwise, it packs - # them based on their receive timestamp. Default: false - # openTransactionSort = false - - # The threshold for the number of broadcast transactions received from each peer every second, - # transactions exceeding this threshold will be discarded - # maxTps = 1000 - isOpenFullTcpDisconnect = false inactiveThreshold = 600 //seconds @@ -204,14 +101,12 @@ node { active = [ # Active establish connection in any case - # Sample entries: # "ip:port", # "ip:port" ] passive = [ # Passive accept connection in any case - # Sample entries: # "ip:port", # "ip:port" ] @@ -228,12 +123,6 @@ node { solidityPort = 8091 PBFTEnable = true PBFTPort = 8092 - - # The maximum request body size for HTTP API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. - # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). - # Setting to 0 rejects all non-empty request bodies (not "unlimited"). - # maxMessageSize = 4m } rpc { @@ -244,223 +133,51 @@ node { PBFTEnable = true PBFTPort = 50071 - # Number of gRPC thread, default availableProcessors / 2 - # thread = 16 - - # The maximum number of concurrent calls permitted for each incoming connection - # maxConcurrentCallsPerConnection = - - # The HTTP/2 flow control window, default 1MB - # flowControlWindow = - - # Connection being idle for longer than which will be gracefully terminated maxConnectionIdleInMillis = 60000 - - # Connection lasting longer than which will be gracefully terminated - # maxConnectionAgeInMillis = - - # The maximum message size allowed to be received on the server, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. - # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). - # Setting to 0 rejects all non-empty request bodies (not "unlimited"). - # maxMessageSize = 4m - - # The maximum size of header list allowed to be received, default 8192 - # maxHeaderListSize = - - # The number of RST_STREAM frames allowed to be sent per connection per period for grpc, by default there is no limit. - # maxRstStream = - - # The number of seconds per period for grpc - # secondsPerWindow = - - # Transactions can only be broadcast if the number of effective connections is reached. minEffectiveConnection = 1 - # The switch of the reflection service, effective for all gRPC services, used for grpcurl tool. Default: false + # The switch of the reflection service for grpcurl tool. Default: false reflectionService = false } - # number of solidity thread in the FullNode. - # If accessing solidity rpc and http interface timeout, could increase the number of threads, - # The default value is the number of cpu cores of the machine. - # solidity.threads = 8 - - # Limits the maximum percentage (default 75%) of producing block interval - # to provide sufficient time to perform other operations e.g. broadcast block - # blockProducedTimeOut = 75 - - # Limits the maximum number (default 700) of transaction from network layer - # netMaxTrxPerSecond = 700 - - # Whether to enable the node detection function. Default: false - # nodeDetectEnable = false - - # use your ipv6 address for node discovery and tcp connection. Default: false - # enableIpv6 = false - - # if your node's highest block num is below than all your pees', try to acquire new connection. Default: false - # effectiveCheckEnable = false - - # Dynamic loading configuration function, disabled by default dynamicConfig = { # enable = false - # checkInterval = 600 // Check interval of Configuration file's change, default is 600 seconds + # checkInterval = 600 } - # Whether to continue broadcast transactions after at least maxUnsolidifiedBlocks are not solidified. Default: false - # unsolidifiedBlockCheck = false - # maxUnsolidifiedBlocks = 54 - dns { - # dns urls to get nodes, url format tree://{pubkey}@{domain}, default empty treeUrls = [ #"tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", ] - - # enable or disable dns publish. Default: false - # publish = false - - # dns domain to publish nodes, required if publish is true - # dnsDomain = "nodes1.example.org" - - # dns private key used to publish, required if publish is true, hex string of length 64 - # dnsPrivate = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" - - # known dns urls to publish if publish is true, url format tree://{pubkey}@{domain}, default empty - # knownUrls = [ - #"tree://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@nodes2.example.org", - # ] - - # staticNodes = [ - # static nodes to published on dns - # Sample entries: - # "ip:port", - # "ip:port" - # ] - - # merge several nodes into a leaf of tree, should be 1~5 - # maxMergeSize = 5 - - # only nodes change percent is bigger then the threshold, we update data on dns - # changeThreshold = 0.1 - - # dns server to publish, required if publish is true, only aws or aliyun is support - # serverType = "aws" - - # access key id of aws or aliyun api, required if publish is true, string - # accessKeyId = "your-key-id" - - # access key secret of aws or aliyun api, required if publish is true, string - # accessKeySecret = "your-key-secret" - - # if publish is true and serverType is aliyun, it's endpoint of aws dns server, string - # aliyunDnsEndpoint = "alidns.aliyuncs.com" - - # if publish is true and serverType is aws, it's region of aws api, such as "eu-south-1", string - # awsRegion = "us-east-1" - - # if publish is true and server-type is aws, it's host zone id of aws's domain, string - # awsHostZoneId = "your-host-zone-id" } - # open the history query APIs(http&GRPC) when node is a lite FullNode, - # like {getBlockByNum, getBlockByID, getTransactionByID...}. Default: false. - # note: above APIs may return null even if blocks and transactions actually are on the blockchain - # when opening on a lite FullNode. only open it if the consequences being clearly known - # openHistoryQueryWhenLiteFN = false - jsonrpc { - # The maximum request body size for JSON-RPC API, default 4M (4194304 bytes). - # Supports human-readable sizes: 4m, 4MB, 4194304. - # Must be non-negative and <= 2147483647 (Integer.MAX_VALUE, ~2 GiB). - # Setting to 0 rejects all non-empty request bodies (not "unlimited"). - # maxMessageSize = 4m - - # 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 - - # The maximum blocks range to retrieve logs for eth_getLogs, default: 5000, <=0 means no limit + httpFullNodeEnable = false + httpFullNodePort = 8545 + maxBlockRange = 5000 - # Allowed max address count in filter request, default: 1000, <=0 means no limit maxAddressSize = 1000 - # The maximum number of allowed topics within a topic criteria, default: 1000, <=0 means no limit maxSubTopics = 1000 - # Allowed maximum number for blockFilter, default: 50000, <=0 means no limit maxBlockFilterNum = 50000 - # Allowed batch size, default: 100, <=0 means no limit maxBatchSize = 100 - # Allowed max response byte size, default: 26214400 (25 MB), <=0 means no limit maxResponseSize = 26214400 - # Allowed maximum number for newFilter, <=0 means no limit maxLogFilterNum = 20000 - # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize. - maxMessageSize = 4M + maxMessageSize = 4194304 } - # Disabled api list, it will work for http, rpc and pbft, both FullNode and SolidityNode, - # but not jsonrpc. The setting is case insensitive, GetNowBlock2 is equal to getnowblock2 disabledApi = [ # "getaccount", # "getnowblock2" ] - } ## rate limiter config rate.limiter = { - # Every api could only set a specific rate limit strategy. Three non-blocking strategy are supported: - # GlobalPreemptibleAdapter: The number of preemptible resource or maximum concurrent requests globally. - # QpsRateLimiterAdapter: qps is the average request count in one second supported by the server, it could be a Double or a Integer. - # IPQPSRateLimiterAdapter: similar to the QpsRateLimiterAdapter, qps could be a Double or a Integer. - # If not set, QpsRateLimiterAdapter with qps=1000 is the default strategy. - # - # Sample entries: - # + # See reference.conf for available strategies (GlobalPreemptibleAdapter, QpsRateLimiterAdapter, IPQPSRateLimiterAdapter). http = [ - # { - # component = "GetNowBlockServlet", - # strategy = "GlobalPreemptibleAdapter", - # paramString = "permit=1" - # }, - - # { - # component = "GetAccountServlet", - # strategy = "IPQPSRateLimiterAdapter", - # paramString = "qps=1" - # }, - - # { - # component = "ListWitnessesServlet", - # strategy = "QpsRateLimiterAdapter", - # paramString = "qps=1" - # } ], rpc = [ - # { - # component = "protocol.Wallet/GetBlockByLatestNum2", - # strategy = "GlobalPreemptibleAdapter", - # paramString = "permit=1" - # }, - - # { - # component = "protocol.Wallet/GetAccount", - # strategy = "IPQPSRateLimiterAdapter", - # paramString = "qps=1" - # }, - - # { - # component = "protocol.Wallet/ListWitnesses", - # strategy = "QpsRateLimiterAdapter", - # paramString = "qps=1" - # }, ] p2p = { @@ -473,6 +190,8 @@ rate.limiter = { global.qps = 50000 # IP-based global qps, default 10000 global.ip.qps = 10000 + # If true, API rate limiters reject immediately on overload (non-blocking). Default: false + apiNonBlocking = false } @@ -480,11 +199,6 @@ rate.limiter = { seed.node = { # List of the seed nodes # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] ip.list = [ "3.225.171.164:18888", "52.8.46.215:18888", @@ -687,10 +401,10 @@ genesis.block = { parentHash = "0xe58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f" } -# Optional. The default is empty. It is used when the witness account has set the witnessPermission. -# When it is not empty, the localWitnessAccountAddress represents the address of the witness account, -# and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. -# When it is empty,the localwitness is configured with the private key of the witness account. +# Optional. Used when the witness account has set witnessPermission. +# localWitnessAccountAddress is the witness account address; +# localwitness is configured with the private key of the witnessPermissionAddress. +# When empty, localwitness is the private key of the witness account itself. # localWitnessAccountAddress = localwitness = [ @@ -704,100 +418,24 @@ block = { needSyncCheck = true maintenanceTimeInterval = 21600000 // 6 hours: 21600000(ms) proposalExpireTime = 259200000 // default value: 3 days: 259200000(ms), Note: this value is controlled by committee proposal - # checkFrozenTime = 1 // for test only } # Transaction reference block, default is "solid", configure to "head" may cause TaPos error trx.reference.block = "solid" // "head" or "solid" -# This property sets the number of milliseconds after the creation of the transaction that is expired, default value is 60000. -# trx.expiration.timeInMilliseconds = 60000 - vm = { supportConstant = false maxEnergyLimitForConstant = 100000000 minTimeRatio = 0.0 maxTimeRatio = 5.0 saveInternalTx = false - # lruCacheSize = 500 - # vmTrace = false - - # Indicates whether the node stores featured internal transactions, such as freeze, vote and so on. Default: false. - # saveFeaturedInternalTx = false - - # Indicates whether the node stores the details of the internal transactions generated by the CANCELALLUNFREEZEV2 opcode, - # such as bandwidth/energy/tronpower cancel amount. Default: false. - # saveCancelAllUnfreezeV2Details = false - - # In rare cases, transactions that will be within the specified maximum execution time (default 10(ms)) are re-executed and packaged - # longRunningTime = 10 - - # Indicates whether the node support estimate energy API. Default: false. - # estimateEnergy = false - - # Indicates the max retry time for executing transaction in estimating energy. Default 3. - # estimateEnergyMaxRetry = 3 - - # Max TVM execution time (ms) for constant calls — applies to - # triggerconstantcontract, triggersmartcontract dispatched to view/pure - # functions, estimateenergy, eth_call, eth_estimateGas, and any other RPC - # routed through the constant-call path. When set, must be a positive - # integer that fits VM deadline conversion and is used verbatim as the - # per-call deadline (no clamp against the network's maxCpuTimeOfOneTx). - # Omit the property entirely to keep the default behaviour of sharing the - # block-processing deadline. Migration note: if previously running --debug - # to extend constant calls, switch to this option (--debug also extends - # block-processing, which is unsafe; see issue #6266). - # constantCallTimeoutMs = 100 } # These parameters are designed for private chain testing only and cannot be freely switched on or off in production systems. +# In production, they are controlled by on-chain committee proposals. committee = { allowCreationOfContracts = 0 //mainnet:0 (reset by committee),test:1 allowAdaptiveEnergy = 0 //mainnet:0 (reset by committee),test:1 - # 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 = 0 - # allowTransactionFeePool = 0 - # allowBlackHoleOptimization = 0 - # allowNewResourceModel = 0 - # allowReceiptsMerkleRoot = 0 - # allowTvmFreeze = 0 - # allowTvmVote = 0 - # unfreezeDelayDays = 0 - # allowTvmLondon = 0 - # allowTvmCompatibleEvm = 0 - # allowNewRewardAlgorithm = 0 - # allowAccountAssetOptimization = 0 - # allowAssetOptimization = 0 - # allowNewReward = 0 - # memoFee = 0 - # allowDelegateOptimization = 0 - # allowDynamicEnergy = 0 - # dynamicEnergyThreshold = 0 - # dynamicEnergyMaxFactor = 0 - # allowTvmShangHai = 0 - # allowOldRewardOpt = 0 - # allowEnergyAdjustment = 0 - # allowStrictMath = 0 - # allowTvmCancun = 0 - # allowTvmBlob = 0 - # consensusLogicOptimization = 0 - # allowOptimizedReturnValueOfChainId = 0 } event.subscribe = { @@ -808,17 +446,13 @@ event.subscribe = { sendqueuelength = 1000 //max length of send queue } version = 0 - # Specify the starting block number to sync historical events. This is only applicable when version = 1. + # 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 = 1 path = "" // absolute path of plugin - server = "" // target server address to receive event triggers - # dbname|username|password, if you want to create indexes for collections when the collections - # are not exist, you can add version and set it to 2, as dbname|username|password|version - # if you use version 2 and one collection not exists, it will create index automaticaly; - # if you use version 2 and one collection exists, it will not create index, you must create index manually; - dbconfig = "" + server = "" // target server address to receive event triggers, "ip:port" + dbconfig = "" // dbname|username|password (append |2 to auto-create indexes on missing collections) contractParse = true topics = [ { @@ -847,7 +481,7 @@ event.subscribe = { }, { triggerName = "solidity" // solidity block trigger(just include solidity block number and timestamp), the value can't be modified - enable = true // Default: true + enable = false topic = "solidity" }, { diff --git a/framework/src/test/java/org/tron/common/ParameterTest.java b/framework/src/test/java/org/tron/common/ParameterTest.java index 563f487f635..0b66c96462c 100644 --- a/framework/src/test/java/org/tron/common/ParameterTest.java +++ b/framework/src/test/java/org/tron/common/ParameterTest.java @@ -176,8 +176,6 @@ public void testCommonParameter() { assertEquals(2, parameter.getEstimateEnergyMaxRetry()); parameter.setKeepAliveInterval(1000); assertEquals(1000, parameter.getKeepAliveInterval()); - parameter.setReceiveTcpMinDataLength(10); - assertEquals(10, parameter.getReceiveTcpMinDataLength()); parameter.setOpenFullTcpDisconnect(false); assertFalse(parameter.isOpenFullTcpDisconnect()); parameter.setNodeDetectEnable(false); @@ -216,7 +214,6 @@ public void testCommonParameter() { assertEquals(1000, parameter.getRateLimiterGlobalQps()); parameter.setRateLimiterGlobalIpQps(100); assertEquals(100, parameter.getRateLimiterGlobalIpQps()); - assertNull(parameter.getOverlay()); assertNull(parameter.getEventPluginConfig()); assertNull(parameter.getEventFilter()); parameter.setCryptoEngine(ECKey_ENGINE); diff --git a/framework/src/test/java/org/tron/common/TestConstants.java b/framework/src/test/java/org/tron/common/TestConstants.java index 88f28688936..a6bf88434ed 100644 --- a/framework/src/test/java/org/tron/common/TestConstants.java +++ b/framework/src/test/java/org/tron/common/TestConstants.java @@ -23,11 +23,7 @@ public class TestConstants { public static final String TEST_CONF = "config-test.conf"; - public static final String NET_CONF = "config.conf"; - public static final String MAINNET_CONF = "config-test-mainnet.conf"; - public static final String LOCAL_CONF = "config-localtest.conf"; - public static final String STORAGE_CONF = "config-test-storagetest.conf"; - public static final String INDEX_CONF = "config-test-index.conf"; + public static final String SHIELD_CONF = "config-shield.conf"; /** * Skips the current test on ARM64 where LevelDB JNI is unavailable. diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java index 64108943ad5..145eda6d398 100644 --- a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -1,8 +1,12 @@ package org.tron.common.jetty; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.servlet.http.HttpServlet; @@ -33,7 +37,7 @@ /** * Tests {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size - * enforcement configured in {@link HttpService#initContextHandler()}. + * enforcement configured in {@link HttpService}. * * Covers: accept/reject by size, UTF-8 byte counting, independent limits * across HttpService instances, chunked transfer, and zero-limit behavior. @@ -152,10 +156,69 @@ public void testHttpBodyWithinLimit() throws Exception { Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); } + /** + * An oversized request must return 413 carrying the short, uniform bad-message + * page produced by the custom ErrorHandler in {@link HttpService} - + * not the default Jetty error page that leaks the exception stack, class name + * and the internal request size. + */ @Test public void testHttpBodyExceedsLimit() throws Exception { - Assert.assertEquals(413, - post(httpServerUri, new StringEntity(repeat('a', HTTP_MAX_BODY_SIZE + 1)))); + HttpPost req = new HttpPost(httpServerUri); + req.setEntity(new StringEntity(repeat('a', HTTP_MAX_BODY_SIZE + 1))); + HttpResponse resp = client.execute(req); + + String body = EntityUtils.toString(resp.getEntity()); + + // return value: 413 + Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + + // returned page: short uniform bad-message page with the generic reason + Assert.assertTrue("should render the short bad-message page", + body.contains("Bad Message 413")); + Assert.assertTrue("reason should be the generic status message", + body.contains("Payload Too Large")); + + // must NOT leak Jetty internals + Assert.assertFalse("must not leak exception class / stack frames", + body.contains("org.eclipse.jetty")); + } + + /** + * A malformed Content-Length is rejected by the HTTP parser (onBadMessage -> + * ErrorHandler.badMessageError()), a different path from the 413 dispatch handler. + * Confirms the custom ErrorHandler leaves other 4xx untouched: still the default + * 400 bad-message page, not rerouted through the 413 branch. + */ + @Test + public void testBadContentLengthReturnsDefault400() throws Exception { + String raw = "POST / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Length: +450\r\n" + + "\r\n"; + String resp = sendRaw(httpServerUri, raw); + + Assert.assertTrue("expected 400, got: " + firstLine(resp), resp.startsWith("HTTP/1.1 400")); + Assert.assertTrue("should be the default bad-message page", resp.contains("Bad Message 400")); + Assert.assertFalse("must not be rerouted through the 413 branch", + resp.contains("Payload Too Large")); + } + + /** + * A request-line URI longer than the request header buffer is rejected by the HTTP + * parser (414), again via badMessageError(), not the 413 dispatch handler. Confirms it + * is unaffected by the custom ErrorHandler. + */ + @Test + public void testOversizedUriReturnsDefault414() throws Exception { + String raw = "GET /" + repeat('a', 9000) + " HTTP/1.1\r\n" // request line > default 8KB + + "Host: localhost\r\n" + + "\r\n"; + String resp = sendRaw(httpServerUri, raw); + + Assert.assertTrue("expected 414, got: " + firstLine(resp), resp.startsWith("HTTP/1.1 414")); + Assert.assertFalse("must not be rerouted through the 413 branch", + resp.contains("Payload Too Large")); } @Test @@ -324,4 +387,26 @@ private int post(URI uri, HttpEntity entity) throws Exception { private static String repeat(char c, int n) { return new String(new char[n]).replace('\0', c); } + + /** Sends a raw HTTP request over a socket and returns the full response (until EOF). */ + private static String sendRaw(URI uri, String rawRequest) throws Exception { + try (Socket socket = new Socket(uri.getHost(), uri.getPort())) { + socket.getOutputStream().write(rawRequest.getBytes(StandardCharsets.US_ASCII)); + socket.getOutputStream().flush(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream in = socket.getInputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } + + /** First line (status line) of a raw HTTP response. */ + private static String firstLine(String resp) { + int idx = resp.indexOf("\r\n"); + return idx < 0 ? resp : resp.substring(0, idx); + } } diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java index 1e5268ddeb6..958af4f7b7b 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventLoaderTest.java @@ -3,11 +3,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import org.junit.Assert; import org.junit.Test; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; import org.tron.common.logsfilter.trigger.BlockLogTrigger; import org.tron.common.logsfilter.trigger.TransactionLogTrigger; @@ -48,6 +53,32 @@ public void launchNativeQueue() { EventPluginLoader.getInstance().stopPlugin(); } + @Test + public void testIsPluginVersionSupported() { + assertEquals("3.0.0", EventPluginLoader.MIN_PLUGIN_VERSION); + // last releases before fastjson removal — must be rejected + assertFalse(checkVersion("1.0.0")); + assertFalse(checkVersion("2.2.0")); + assertFalse(checkVersion("2.9.9")); + // 3.0.0 onward — must be accepted + assertTrue(checkVersion("3.0.0")); + assertTrue(checkVersion("3.1.5")); + assertTrue(checkVersion("10.0.0")); + // empty/null version — reject + assertFalse(checkVersion("")); + assertFalse(checkVersion(null)); + } + + private static boolean checkVersion(String version) { + PluginManager pm = mock(PluginManager.class); + PluginWrapper wrapper = mock(PluginWrapper.class); + PluginDescriptor desc = mock(PluginDescriptor.class); + when(pm.getPlugin("test")).thenReturn(wrapper); + when(wrapper.getDescriptor()).thenReturn(desc); + when(desc.getVersion()).thenReturn(version); + return EventPluginLoader.isPluginVersionSupported(pm, "test"); + } + @Test public void testBlockLogTrigger() { BlockLogTrigger blt = new BlockLogTrigger(); diff --git a/framework/src/test/java/org/tron/common/logsfilter/TransactionLogTriggerCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/TransactionLogTriggerCapsuleTest.java index ce0f63ef7a4..1d559adb6d0 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/TransactionLogTriggerCapsuleTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/TransactionLogTriggerCapsuleTest.java @@ -35,6 +35,24 @@ public void setup() { System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); } + @Test + public void testSetRemoved() { + BalanceContract.TransferContract.Builder builder = + BalanceContract.TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString(RECEIVER_ADDRESS))) + .setAmount(1000L); + transactionCapsule = new TransactionCapsule(builder.build(), + Protocol.Transaction.Contract.ContractType.TransferContract); + TransactionLogTriggerCapsule triggerCapsule = + new TransactionLogTriggerCapsule(transactionCapsule, blockCapsule); + + // default is false (forward emit); reorg rollback sets it to true + Assert.assertFalse(triggerCapsule.getTransactionLogTrigger().isRemoved()); + triggerCapsule.setRemoved(true); + Assert.assertTrue(triggerCapsule.getTransactionLogTrigger().isRemoved()); + } + @Test public void testConstructorWithUnfreezeBalanceTrxCapsule() { BalanceContract.UnfreezeBalanceContract.Builder builder2 = diff --git a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsuleTest.java index f77869b8650..60e51144a40 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsuleTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockLogTriggerCapsuleTest.java @@ -32,4 +32,12 @@ public void testSetLatestSolidifiedBlockNumber() { Assert.assertEquals(100, blockLogTriggerCapsule.getBlockLogTrigger().getLatestSolidifiedBlockNumber()); } + + @Test + public void testSetRemoved() { + // default is false (forward emit); reorg rollback sets it to true + Assert.assertFalse(blockLogTriggerCapsule.getBlockLogTrigger().isRemoved()); + blockLogTriggerCapsule.setRemoved(true); + Assert.assertTrue(blockLogTriggerCapsule.getBlockLogTrigger().isRemoved()); + } } diff --git a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java new file mode 100644 index 00000000000..ab147f57a79 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java @@ -0,0 +1,88 @@ +package org.tron.common.runtime; + +import static org.mockito.ArgumentMatchers.any; + +import java.lang.reflect.Field; +import java.util.Collections; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.runtime.vm.LogInfo; +import org.tron.core.actuator.VMActuator; +import org.tron.core.db.TransactionContext; +import org.tron.core.vm.OperationRegistry; +import org.tron.core.vm.VM; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program; + +public class VMActuatorMockTest { + + @BeforeClass + public static void init() { + // warm up the registry so VM.play(..., OperationRegistry.getTable()) arg eval is safe + OperationRegistry.init(); + } + + private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expectedSize) + throws Exception { + boolean prevOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmOsaka(osakaOn ? 1 : 0); + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + ProgramResult result = new ProgramResult(); + result.addLogInfo(new LogInfo(new byte[20], Collections.emptyList(), new byte[0])); + result.addDeleteAccount(new DataWord(1)); + Mockito.when(program.getResult()).thenReturn(result); + + vmMock.when(() -> VM.play(any(), any())).thenThrow(thrownByVm); + + VMActuator actuator = new VMActuator(false); + Field f = VMActuator.class.getDeclaredField("program"); + f.setAccessible(true); + f.set(actuator, program); + + TransactionContext context = Mockito.mock(TransactionContext.class); + Mockito.when(context.getProgramResult()).thenReturn(new ProgramResult()); + + actuator.execute(context); + + Assert.assertEquals(expectedSize, result.getLogInfoList().size()); + Assert.assertEquals(expectedSize, result.getDeleteAccounts().size()); + } finally { + VMConfig.initAllowTvmOsaka(prevOsaka ? 1 : 0); + } + } + + @Test + public void osakaClearsLogOnOutOfTime() throws Exception { + runCatchPathTest(new Program.OutOfTimeException("timeout"), true, 0); + } + + @Test + public void osakaClearsLogOnJvmStackOverflow() throws Exception { + runCatchPathTest(new Program.JVMStackOverFlowException(), true, 0); + } + + @Test + public void osakaClearsLogOnThrowable() throws Exception { + runCatchPathTest(new RuntimeException("boom"), true, 0); + } + + @Test + public void preOsakaKeepsLogOnOutOfTime() throws Exception { + runCatchPathTest(new Program.OutOfTimeException("timeout"), false, 1); + } + + @Test + public void preOsakaKeepsLogOnJvmStackOverflow() throws Exception { + runCatchPathTest(new Program.JVMStackOverFlowException(), false, 1); + } + + @Test + public void preOsakaKeepsLogOnThrowable() throws Exception { + runCatchPathTest(new RuntimeException("boom"), false, 1); + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/AllowTvmOsakaTest.java b/framework/src/test/java/org/tron/common/runtime/vm/AllowTvmOsakaTest.java index c7000175b00..8e2ab59b1f7 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/AllowTvmOsakaTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/AllowTvmOsakaTest.java @@ -80,6 +80,46 @@ private static long getEnergy(int baseLen, int expLen, int modLen, byte[] expVal return modExp.getEnergyForData(buildModExpData(baseLen, expLen, modLen, expValue)); } + @Test + public void testModExpZeroModulusOutputLengthGatedByOsaka() { + ConfigLoader.disable = true; + + byte[] modLenZero = buildModExpData(1, 1, 0, new byte[]{0x03}); + byte[] modLenOne = buildModExpData(1, 1, 1, new byte[]{0x03}); + byte[] modLen32 = buildModExpData(1, 1, 32, new byte[]{0x03}); + + try { + VMConfig.initAllowTvmOsaka(0); + Pair result = modExp.execute(modLenZero); + Assert.assertTrue(result.getLeft()); + Assert.assertEquals(0, result.getRight().length); + + result = modExp.execute(modLenOne); + Assert.assertTrue(result.getLeft()); + Assert.assertEquals(0, result.getRight().length); + + result = modExp.execute(modLen32); + Assert.assertTrue(result.getLeft()); + Assert.assertEquals(0, result.getRight().length); + + VMConfig.initAllowTvmOsaka(1); + result = modExp.execute(modLenZero); + Assert.assertTrue(result.getLeft()); + Assert.assertEquals(0, result.getRight().length); + + result = modExp.execute(modLenOne); + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(new byte[1], result.getRight()); + + result = modExp.execute(modLen32); + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(new byte[32], result.getRight()); + } finally { + VMConfig.initAllowTvmOsaka(0); + ConfigLoader.disable = false; + } + } + @Test public void testEIP7883ModExpPricing() { ConfigLoader.disable = true; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeTest.java index 582f5157b27..86b26c24672 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeTest.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.Commons; @@ -61,7 +62,6 @@ public class BandWidthRuntimeOutOfTimeTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static final String dbDirectory = "db_BandWidthRuntimeOutOfTimeTest_test"; - private static final String indexDirectory = "index_BandWidthRuntimeOutOfTimeTest_test"; private static final String OwnerAddress = "TCWHANtDDdkZCTo2T2peyEq3Eg9c2XB7ut"; private static final String TriggerOwnerAddress = "TCSgeWapPJhCqgWRxXCKb6jJ5AgNWSGjPA"; @@ -72,10 +72,9 @@ public class BandWidthRuntimeOutOfTimeTest extends BaseTest { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeWithCheckTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeWithCheckTest.java index 7e75f2b31d1..bb5fbf36d55 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeWithCheckTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeOutOfTimeWithCheckTest.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.Commons; @@ -63,7 +64,6 @@ public class BandWidthRuntimeOutOfTimeWithCheckTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static final String dbDirectory = "db_BandWidthRuntimeOutOfTimeTest_test"; - private static final String indexDirectory = "index_BandWidthRuntimeOutOfTimeTest_test"; private static final String OwnerAddress = "TCWHANtDDdkZCTo2T2peyEq3Eg9c2XB7ut"; private static final String TriggerOwnerAddress = "TCSgeWapPJhCqgWRxXCKb6jJ5AgNWSGjPA"; private static boolean init; @@ -73,10 +73,9 @@ public class BandWidthRuntimeOutOfTimeWithCheckTest extends BaseTest { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java index 8e38c08c4d8..fb682bcb50f 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeTest.java @@ -24,6 +24,7 @@ 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.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; @@ -57,7 +58,6 @@ public class BandWidthRuntimeTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static final String dbDirectory = "db_BandWidthRuntimeTest_test"; - private static final String indexDirectory = "index_BandWidthRuntimeTest_test"; private static final String OwnerAddress = "TCWHANtDDdkZCTo2T2peyEq3Eg9c2XB7ut"; private static final String TriggerOwnerAddress = "TCSgeWapPJhCqgWRxXCKb6jJ5AgNWSGjPA"; private static final String TriggerOwnerTwoAddress = "TPMBUANrTwwQAPwShn7ZZjTJz1f3F8jknj"; @@ -69,9 +69,8 @@ public static void init() { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeWithCheckTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeWithCheckTest.java index aae8cb5702d..a05d6603874 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeWithCheckTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BandWidthRuntimeWithCheckTest.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.Commons; @@ -63,7 +64,6 @@ public class BandWidthRuntimeWithCheckTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static final String dbDirectory = "db_BandWidthRuntimeWithCheckTest_test"; - private static final String indexDirectory = "index_BandWidthRuntimeWithCheckTest_test"; private static final String OwnerAddress = "TCWHANtDDdkZCTo2T2peyEq3Eg9c2XB7ut"; private static final String TriggerOwnerAddress = "TCSgeWapPJhCqgWRxXCKb6jJ5AgNWSGjPA"; private static final String TriggerOwnerTwoAddress = "TPMBUANrTwwQAPwShn7ZZjTJz1f3F8jknj"; @@ -75,9 +75,8 @@ public class BandWidthRuntimeWithCheckTest extends BaseTest { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } 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/common/runtime/vm/OperationsTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java index 651248bd9e4..a1627f4f2e2 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java @@ -788,6 +788,72 @@ Op.CALL, new DataWord(10000), VMConfig.initAllowTvmSelfdestructRestriction(0); } + @Test + public void testCreate2MaxDepthWithOsakaOnly() throws ContractValidateException { + boolean allowTvmCompatibleEvm = VMConfig.allowTvmCompatibleEvm(); + boolean allowTvmOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmCompatibleEvm(0); + VMConfig.initAllowTvmOsaka(1); + try { + invoke = new ProgramInvokeMockImpl() { + @Override + public int getCallDeep() { + return 64; + } + }; + Protocol.Transaction trx = Protocol.Transaction.getDefaultInstance(); + InternalTransaction interTrx = + new InternalTransaction(trx, InternalTransaction.TrxType.TRX_UNKNOWN_TYPE); + program = new Program(new byte[0], new byte[0], invoke, interTrx); + program.setRootTransactionId(new byte[32]); + + program.createContract2(DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO()); + + Assert.assertEquals(DataWord.ZERO(), program.getStack().pop()); + Assert.assertTrue(program.getResult().getInternalTransactions().isEmpty()); + } finally { + VMConfig.initAllowTvmCompatibleEvm(allowTvmCompatibleEvm ? 1 : 0); + VMConfig.initAllowTvmOsaka(allowTvmOsaka ? 1 : 0); + } + } + + @Test + public void testCreate2MaxDepthWithNeitherFlag() throws ContractValidateException { + boolean allowTvmCompatibleEvm = VMConfig.allowTvmCompatibleEvm(); + boolean allowTvmOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmCompatibleEvm(0); + VMConfig.initAllowTvmOsaka(0); + try { + byte[] contractAddr = Hex.decode("41471fd3ad3e9eeadeec4608b92d16ce6b500704cc"); + invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr) { + @Override + public int getCallDeep() { + return 64; + } + + @Override + public boolean byTestingSuite() { + return true; + } + }; + program = new Program(null, null, invoke, + new InternalTransaction(Protocol.Transaction.getDefaultInstance(), + InternalTransaction.TrxType.TRX_UNKNOWN_TYPE)); + program.setRootTransactionId(new byte[32]); + + program.createContract2(DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO()); + + // With neither flag enabled the MAX_DEPTH short-circuit must not fire: CREATE2 + // proceeds, records an internal transaction and pushes the new contract + // address (not 0), unlike the Osaka/CompatibleEvm guarded path above. + Assert.assertFalse(program.getResult().getInternalTransactions().isEmpty()); + Assert.assertFalse(program.getStack().pop().isZero()); + } finally { + VMConfig.initAllowTvmCompatibleEvm(allowTvmCompatibleEvm ? 1 : 0); + VMConfig.initAllowTvmOsaka(allowTvmOsaka ? 1 : 0); + } + } + // TIP-854 outer-frame containment: a CALL to validateMultiSign or // batchValidateSign with malformed calldata must (a) push 0 onto the outer // stack, (b) leave the outer frame free of any propagated exception, and diff --git a/framework/src/test/java/org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.java b/framework/src/test/java/org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.java index 27e7891e6d8..080441bfaf4 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.java @@ -15,6 +15,7 @@ import org.junit.Test; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.FileUtil; @@ -60,7 +61,7 @@ public class PrecompiledContractsVerifyProofTest extends BaseTest { @BeforeClass public static void init() { - Args.setParam(new String[]{"--output-directory", dbPath()}, "config-test.conf"); + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); DEFAULT_OVK = ByteArray .fromHexString("030c8c2bc59fb3eb8afb047a8ea4b028743d23e7d38c6fa30908358431e2314d"); SHIELDED_CONTRACT_ADDRESS = WalletClient.decodeFromBase58Check(SHIELDED_CONTRACT_ADDRESS_STR); @@ -1125,6 +1126,124 @@ public void verifyBurnWrongDataLength() throws ZksnarkException { Assert.assertEquals(0, result[31]); } + @Test + public void buildBurnRejectsInvalidTransparentToAddress() throws ZksnarkException { + long value = 100L; + SpendingKey senderSk = SpendingKey.random(); + ExpandedSpendingKey senderExpsk = senderSk.expandedSpendingKey(); + FullViewingKey senderFvk = senderSk.fullViewingKey(); + IncomingViewingKey senderIvk = senderFvk.inViewingKey(); + byte[] rcm = new byte[32]; + JLibrustzcash.librustzcashSaplingGenerateR(rcm); + PaymentAddress senderPaymentAddress = senderIvk.address(DiversifierT.random()).orElse(null); + assertNotNull(senderPaymentAddress); + + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder(); + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + builder.setShieldedTRC20Address(SHIELDED_CONTRACT_ADDRESS); + builder.setTransparentToAmount(BigInteger.valueOf(value)); + builder.setTransparentToAddress(new byte[19]); + + Note senderNote = new Note(senderPaymentAddress.getD(), senderPaymentAddress.getPkD(), + value, rcm, new byte[512]); + byte[][] cm = new byte[1][32]; + System.arraycopy(senderNote.cm(), 0, cm[0], 0, 32); + IncrementalMerkleTreeContainer tree = new IncrementalMerkleTreeContainer( + new IncrementalMerkleTreeCapsule()); + IncrementalMerkleVoucherContainer voucher = addSimpleMerkleVoucherContainer(tree, cm); + byte[] path = decodePath(voucher.path().encode()); + byte[] anchor = voucher.root().getContent().toByteArray(); + long position = voucher.position(); + builder.addSpend(senderExpsk, senderNote, anchor, path, position); + + try { + builder.build(true); + Assert.fail("expected ZksnarkException for invalid transparentToAddress"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage().contains("invalid transparentToAddress")); + } + } + + @Test + public void buildBurnRejectsMissingOvk() throws ZksnarkException { + long value = 100L; + SpendingKey senderSk = SpendingKey.random(); + ExpandedSpendingKey senderExpsk = senderSk.expandedSpendingKey(); + FullViewingKey senderFvk = senderSk.fullViewingKey(); + IncomingViewingKey senderIvk = senderFvk.inViewingKey(); + byte[] rcm = new byte[32]; + JLibrustzcash.librustzcashSaplingGenerateR(rcm); + PaymentAddress senderPaymentAddress = senderIvk.address(DiversifierT.random()).orElse(null); + assertNotNull(senderPaymentAddress); + + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder(); + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + builder.setShieldedTRC20Address(SHIELDED_CONTRACT_ADDRESS); + builder.setTransparentToAmount(BigInteger.valueOf(value)); + builder.setTransparentToAddress(PUBLIC_TO_ADDRESS); + + Note senderNote = new Note(senderPaymentAddress.getD(), senderPaymentAddress.getPkD(), + value, rcm, new byte[512]); + byte[][] cm = new byte[1][32]; + System.arraycopy(senderNote.cm(), 0, cm[0], 0, 32); + IncrementalMerkleTreeContainer tree = new IncrementalMerkleTreeContainer( + new IncrementalMerkleTreeCapsule()); + IncrementalMerkleVoucherContainer voucher = addSimpleMerkleVoucherContainer(tree, cm); + byte[] path = decodePath(voucher.path().encode()); + byte[] anchor = voucher.root().getContent().toByteArray(); + long position = voucher.position(); + builder.addSpend(senderFvk.getAk(), senderExpsk.getNsk(), senderNote, + Note.generateR(), anchor, path, position); + + try { + builder.build(false); + Assert.fail("expected ZksnarkException for missing ovk"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage().contains("missing ovk for burn encryption")); + } + } + + @Test + public void buildBurnRejectsMissingExpandedSpendingKeyForWithAsk() throws ZksnarkException { + long value = 100L; + SpendingKey senderSk = SpendingKey.random(); + ExpandedSpendingKey senderExpsk = senderSk.expandedSpendingKey(); + FullViewingKey senderFvk = senderSk.fullViewingKey(); + IncomingViewingKey senderIvk = senderFvk.inViewingKey(); + byte[] rcm = new byte[32]; + JLibrustzcash.librustzcashSaplingGenerateR(rcm); + PaymentAddress senderPaymentAddress = senderIvk.address(DiversifierT.random()).orElse(null); + assertNotNull(senderPaymentAddress); + + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder(); + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + builder.setShieldedTRC20Address(SHIELDED_CONTRACT_ADDRESS); + builder.setTransparentToAmount(BigInteger.valueOf(value)); + builder.setTransparentToAddress(PUBLIC_TO_ADDRESS); + builder.setOvk(senderFvk.getOvk()); + + Note senderNote = new Note(senderPaymentAddress.getD(), senderPaymentAddress.getPkD(), + value, rcm, new byte[512]); + byte[][] cm = new byte[1][32]; + System.arraycopy(senderNote.cm(), 0, cm[0], 0, 32); + IncrementalMerkleTreeContainer tree = new IncrementalMerkleTreeContainer( + new IncrementalMerkleTreeCapsule()); + IncrementalMerkleVoucherContainer voucher = addSimpleMerkleVoucherContainer(tree, cm); + byte[] path = decodePath(voucher.path().encode()); + byte[] anchor = voucher.root().getContent().toByteArray(); + long position = voucher.position(); + builder.addSpend(senderFvk.getAk(), senderExpsk.getNsk(), senderNote, + Note.generateR(), anchor, path, position); + + try { + builder.build(true); + Assert.fail("expected ZksnarkException for missing expanded spending key"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage().contains( + "missing expanded spending key for spend authorization")); + } + } + @Test public void verifyMintWrongLeafcount() throws ZksnarkException { long value = 100L; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/TvmIssueVerifierTest.java b/framework/src/test/java/org/tron/common/runtime/vm/TvmIssueVerifierTest.java new file mode 100644 index 00000000000..4936ec8f4c8 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/TvmIssueVerifierTest.java @@ -0,0 +1,209 @@ +package org.tron.common.runtime.vm; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.runtime.TVMTestResult; +import org.tron.common.runtime.TvmTestUtils; +import org.tron.common.utils.WalletUtil; +import org.tron.common.utils.client.utils.AbiUtil; +import org.tron.core.exception.ContractExeException; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ReceiptCheckErrException; +import org.tron.core.exception.VMIllegalException; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.protos.Protocol.Transaction; + +public class TvmIssueVerifierTest extends VMTestBase { + + private static final int WORD_SIZE = 32; + + private static final String ABI = + "[{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"code\",\"type\":\"bytes\"}," + + "{\"internalType\":\"uint256\",\"name\":\"salt\",\"type\":\"uint256\"}]," + + "\"name\":\"failedCreate2KeepsPriorReturnData\"," + + "\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"beforeSize\"," + + "\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"created\"," + + "\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"afterSize\"," + + "\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}," + + "{\"inputs\":[],\"name\":\"modexpZeroModulus\"," + + "\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"ok\"," + + "\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"sizeAfter\"," + + "\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"copiedWord\"," + + "\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}," + + "{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"code\",\"type\":\"bytes\"}," + + "{\"internalType\":\"uint256\",\"name\":\"salt\",\"type\":\"uint256\"}]," + + "\"name\":\"successfulCreate2KeepsPriorReturnData\"," + + "\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"beforeSize\"," + + "\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"created\"," + + "\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"afterSize\"," + + "\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"createdSize\"," + + "\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"; + + private static final String BYTECODE = + "6080604052348015600f57600080fd5b506105198061001f6000396000f3fe6080604052348015610010" + + "57600080fd5b50600436106100415760003560e01c80634ecba0f014610046578063543b525514610079" + + "5780639fefb5fd146100ab575b600080fd5b610060600480360381019061005b919061036b565b6100cb" + + "565b6040516100709493929190610417565b60405180910390f35b610093600480360381019061008e91" + + "9061036b565b610104565b6040516100a29392919061045c565b60405180910390f35b6100b361013656" + + "5b6040516100c2939291906104ac565b60405180910390f35b6000806000806112346000526020600060" + + "2060008060045af1503d9350848651602088016000f592503d9150823b905092959194509250565b6000" + + "80600061123460005260206000602060008060045af1503d9250838551602087016001f591503d905092" + + "50925092565b600080600080606367ffffffffffffffff8111156101575761015661020a565b5b604051" + + "9080825280601f01601f1916602001820160405280156101895781602001600182028036833780820191" + + "505090505b50905060208101600181526001602082015260016040820152600260608201536003606182" + + "01536000606282015360001960005260206000606383600060055af194503d9350600051925050509091" + + "92565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301" + + "169050919050565b7f4e487b710000000000000000000000000000000000000000000000000000000060" + + "0052604160045260246000fd5b610242826101f9565b810181811067ffffffffffffffff821117156102" + + "615761026061020a565b5b80604052505050565b60006102746101db565b90506102808282610239565b" + + "919050565b600067ffffffffffffffff8211156102a05761029f61020a565b5b6102a9826101f9565b90" + + "50602081019050919050565b82818337600083830152505050565b60006102d86102d384610285565b61" + + "026a565b9050828152602081018484840111156102f4576102f36101f4565b5b6102ff8482856102b656" + + "5b509392505050565b600082601f83011261031c5761031b6101ef565b5b813561032c84826020860161" + + "02c5565b91505092915050565b6000819050919050565b61034881610335565b811461035357600080fd" + + "5b50565b6000813590506103658161033f565b92915050565b6000806040838503121561038257610381" + + "6101e5565b5b600083013567ffffffffffffffff8111156103a05761039f6101ea565b5b6103ac858286" + + "01610307565b92505060206103bd85828601610356565b9150509250929050565b6103d081610335565b" + + "82525050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006104" + + "01826103d6565b9050919050565b610411816103f6565b82525050565b600060808201905061042c6000" + + "8301876103c7565b6104396020830186610408565b61044660408301856103c7565b6104536060830184" + + "6103c7565b95945050505050565b600060608201905061047160008301866103c7565b61047e60208301" + + "85610408565b61048b60408301846103c7565b949350505050565b6000819050919050565b6104a68161" + + "0493565b82525050565b60006060820190506104c160008301866103c7565b6104ce60208301856103c7" + + "565b6104db604083018461049d565b94935050505056fea2646970667358221220c9b28608a5295f3b52" + + "702e75aa5d40b18593bd0a9ff2e03e2274edbd42642c6a64736f6c634300081e0033"; + + @Before + public void enableVmFeatures() { + ConfigLoader.disable = false; + manager.getDynamicPropertiesStore().saveAllowTvmTransferTrc10(1); + manager.getDynamicPropertiesStore().saveAllowTvmConstantinople(1); + manager.getDynamicPropertiesStore().saveAllowTvmIstanbul(1); + manager.getDynamicPropertiesStore().saveAllowTvmLondon(1); + manager.getDynamicPropertiesStore().saveAllowTvmCompatibleEvm(1); + } + + @Test + public void verifyTvmOsakaFixesWithSolidity() + throws ContractExeException, ReceiptCheckErrException, + VMIllegalException, ContractValidateException { + byte[] verifierAddress = deployVerifier(); + + manager.getDynamicPropertiesStore().saveAllowTvmOsaka(0); + + TVMTestResult modExpResult = + trigger(verifierAddress, "modexpZeroModulus()", Collections.emptyList(), 100_000_000L); + byte[] modExpReturn = modExpResult.getRuntime().getResult().getHReturn(); + Assert.assertNull(modExpResult.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.ONE, word(modExpReturn, 0)); + Assert.assertEquals("MODEXP zero modulus currently returns empty returndata", + BigInteger.ZERO, word(modExpReturn, 1)); + + TVMTestResult create2Result = + trigger(verifierAddress, "failedCreate2KeepsPriorReturnData(bytes,uint256)", + Arrays.asList("00", 7L), 100_000_000L); + byte[] create2Return = create2Result.getRuntime().getResult().getHReturn(); + Assert.assertNull(create2Result.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.valueOf(32), word(create2Return, 0)); + Assert.assertEquals(BigInteger.ZERO, word(create2Return, 1)); + Assert.assertEquals("failed CREATE2 keeps the previous 32-byte return data buffer", + BigInteger.valueOf(32), word(create2Return, 2)); + + TVMTestResult preOsakaCreate2SuccessResult = + trigger(verifierAddress, "successfulCreate2KeepsPriorReturnData(bytes,uint256)", + Arrays.asList(initCodeReturningRuntime("00"), 8L), 100_000_000L); + byte[] preOsakaCreate2SuccessReturn = + preOsakaCreate2SuccessResult.getRuntime().getResult().getHReturn(); + Assert.assertNull(preOsakaCreate2SuccessResult.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.valueOf(32), word(preOsakaCreate2SuccessReturn, 0)); + Assert.assertTrue(word(preOsakaCreate2SuccessReturn, 1).signum() != 0); + Assert.assertEquals(BigInteger.valueOf(32), word(preOsakaCreate2SuccessReturn, 2)); + Assert.assertEquals(BigInteger.ONE, word(preOsakaCreate2SuccessReturn, 3)); + + manager.getDynamicPropertiesStore().saveAllowTvmOsaka(1); + + modExpResult = + trigger(verifierAddress, "modexpZeroModulus()", Collections.emptyList(), 100_000_000L); + modExpReturn = modExpResult.getRuntime().getResult().getHReturn(); + Assert.assertNull(modExpResult.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.ONE, word(modExpReturn, 0)); + Assert.assertEquals("MODEXP zero modulus returns modLen bytes after Osaka", + BigInteger.ONE, word(modExpReturn, 1)); + + create2Result = + trigger(verifierAddress, "failedCreate2KeepsPriorReturnData(bytes,uint256)", + Arrays.asList("00", 7L), 100_000_000L); + create2Return = create2Result.getRuntime().getResult().getHReturn(); + Assert.assertNull(create2Result.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.valueOf(32), word(create2Return, 0)); + Assert.assertEquals(BigInteger.ZERO, word(create2Return, 1)); + Assert.assertEquals("failed CREATE2 clears the previous return data buffer after Osaka", + BigInteger.ZERO, word(create2Return, 2)); + + TVMTestResult create2SuccessResult = + trigger(verifierAddress, "successfulCreate2KeepsPriorReturnData(bytes,uint256)", + Arrays.asList(initCodeReturningRuntime("00"), 9L), 100_000_000L); + byte[] create2SuccessReturn = create2SuccessResult.getRuntime().getResult().getHReturn(); + Assert.assertNull(create2SuccessResult.getRuntime().getRuntimeError()); + Assert.assertEquals(BigInteger.valueOf(32), word(create2SuccessReturn, 0)); + Assert.assertTrue(word(create2SuccessReturn, 1).signum() != 0); + Assert.assertEquals("successful CREATE2 clears the previous return data buffer after Osaka", + BigInteger.ZERO, word(create2SuccessReturn, 2)); + Assert.assertEquals(BigInteger.ONE, word(create2SuccessReturn, 3)); + } + + private byte[] deployVerifier() + throws ContractExeException, ReceiptCheckErrException, + VMIllegalException, ContractValidateException { + byte[] owner = Hex.decode(OWNER_ADDRESS); + Transaction trx = TvmTestUtils.generateDeploySmartContractAndGetTransaction( + "TvmIssueVerifier", owner, ABI, BYTECODE, 0, 1_000_000_000L, 0, null); + byte[] contractAddress = WalletUtil.generateContractAddress(trx); + Assert.assertNull(TvmTestUtils + .processTransactionAndReturnRuntime(trx, rootRepository, null) + .getRuntimeError()); + return contractAddress; + } + + private TVMTestResult trigger(byte[] contractAddress, String method, List args, + long feeLimit) + throws ContractExeException, ReceiptCheckErrException, + VMIllegalException, ContractValidateException { + String input = AbiUtil.parseMethod(method, args); + return TvmTestUtils.triggerContractAndReturnTvmTestResult(Hex.decode(OWNER_ADDRESS), + contractAddress, Hex.decode(input), 0, feeLimit, manager, null); + } + + private static BigInteger word(byte[] data, int index) { + int start = index * WORD_SIZE; + return new BigInteger(1, Arrays.copyOfRange(data, start, start + WORD_SIZE)); + } + + private static String initCodeReturningRuntime(String runtimeCode) { + byte[] runtime = Hex.decode(runtimeCode); + Assert.assertTrue(runtime.length <= 255); + + byte[] initCode = new byte[12 + runtime.length]; + initCode[0] = 0x60; + initCode[1] = (byte) runtime.length; + initCode[2] = 0x60; + initCode[3] = 0x0c; + initCode[4] = 0x60; + initCode[5] = 0x00; + initCode[6] = 0x39; + initCode[7] = 0x60; + initCode[8] = (byte) runtime.length; + initCode[9] = 0x60; + initCode[10] = 0x00; + initCode[11] = (byte) 0xf3; + System.arraycopy(runtime, 0, initCode, 12, runtime.length); + + return Hex.toHexString(initCode); + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java new file mode 100644 index 00000000000..845db6dd6af --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java @@ -0,0 +1,108 @@ +package org.tron.common.runtime.vm; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.vm.config.VMConfig; + +public class VMConfigIsolationTest { + + // Tests mutate the process-wide static VMConfig; snapshot it before each test and restore it + // after so this class never pollutes other VM tests sharing the same JVM fork (forkEvery=100). + private VMConfig.Snapshot savedGlobal; + + @Before + public void snapshotConfig() { + VMConfig.clearLocalSnapshot(); + savedGlobal = snapshotGlobal(); + } + + @After + public void restoreConfig() { + VMConfig.clearLocalSnapshot(); + VMConfig.setGlobalSnapshot(savedGlobal); + } + + /** + * A constant call's thread-local config view must not pollute the global config that the + * (concurrent) block-processing path reads. This is the core Problem-2 guarantee. + */ + @Test + public void testLocalConfigDoesNotPolluteGlobalAcrossThreads() throws InterruptedException { + VMConfig.initAllowTvmOsaka(1); // global (HEAD) view: activated + assertTrue(VMConfig.allowTvmOsaka()); // no thread-local -> reads global + + VMConfig.Snapshot local = new VMConfig.Snapshot(); + local.allowTvmOsaka = false; // simulate a not-yet-solidified snapshot + VMConfig.setLocalSnapshot(local); + + // this thread now sees its own (solidity) view... + assertFalse(VMConfig.allowTvmOsaka()); + + // ...but another thread (e.g. block processing) must still see the global HEAD value. + AtomicBoolean otherThreadSaw = new AtomicBoolean(false); + Thread t = new Thread(() -> otherThreadSaw.set(VMConfig.allowTvmOsaka())); + t.start(); + t.join(); + assertTrue("global config must be unaffected by another thread's local view", + otherThreadSaw.get()); + + // after dropping the local view, this thread falls back to the global value again. + VMConfig.clearLocalSnapshot(); + assertTrue(VMConfig.allowTvmOsaka()); + } + + /** + * A block/broadcast load (setGlobalSnapshot) must drop any thread-local view, so the consensus + * path can never read a constant call's leaked snapshot left on the same pooled worker thread. + */ + @Test + public void testSetGlobalConfigDropsLocalView() { + VMConfig.Snapshot local = new VMConfig.Snapshot(); + local.allowTvmOsaka = true; + VMConfig.setLocalSnapshot(local); + assertTrue(VMConfig.allowTvmOsaka()); + + VMConfig.Snapshot head = new VMConfig.Snapshot(); + head.allowTvmOsaka = false; + VMConfig.setGlobalSnapshot(head); + assertFalse("setGlobalSnapshot must drop the thread-local view", VMConfig.allowTvmOsaka()); + } + + // Deep-copy the current global config through the public getters (no thread-local set here, so + // the getters read the global) so @After can restore the exact prior state. + private static VMConfig.Snapshot snapshotGlobal() { + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmTransferTrc10 = VMConfig.allowTvmTransferTrc10(); + snapshot.allowTvmConstantinople = VMConfig.allowTvmConstantinople(); + snapshot.allowMultiSign = VMConfig.allowMultiSign(); + snapshot.allowTvmSolidity059 = VMConfig.allowTvmSolidity059(); + snapshot.allowShieldedTRC20Transaction = VMConfig.allowShieldedTRC20Transaction(); + snapshot.allowTvmIstanbul = VMConfig.allowTvmIstanbul(); + snapshot.allowTvmFreeze = VMConfig.allowTvmFreeze(); + snapshot.allowTvmVote = VMConfig.allowTvmVote(); + snapshot.allowTvmLondon = VMConfig.allowTvmLondon(); + snapshot.allowTvmCompatibleEvm = VMConfig.allowTvmCompatibleEvm(); + snapshot.allowHigherLimitForMaxCpuTimeOfOneTx = VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx(); + snapshot.allowTvmFreezeV2 = VMConfig.allowTvmFreezeV2(); + snapshot.allowOptimizedReturnValueOfChainId = VMConfig.allowOptimizedReturnValueOfChainId(); + snapshot.allowDynamicEnergy = VMConfig.allowDynamicEnergy(); + snapshot.dynamicEnergyThreshold = VMConfig.getDynamicEnergyThreshold(); + snapshot.dynamicEnergyIncreaseFactor = VMConfig.getDynamicEnergyIncreaseFactor(); + snapshot.dynamicEnergyMaxFactor = VMConfig.getDynamicEnergyMaxFactor(); + snapshot.allowTvmShanghai = VMConfig.allowTvmShanghai(); + snapshot.allowEnergyAdjustment = VMConfig.allowEnergyAdjustment(); + snapshot.allowStrictMath = VMConfig.allowStrictMath(); + snapshot.allowTvmCancun = VMConfig.allowTvmCancun(); + snapshot.disableJavaLangMath = VMConfig.disableJavaLangMath(); + snapshot.allowTvmBlob = VMConfig.allowTvmBlob(); + snapshot.allowTvmSelfdestructRestriction = VMConfig.allowTvmSelfdestructRestriction(); + snapshot.allowTvmOsaka = VMConfig.allowTvmOsaka(); + snapshot.allowHardenResourceCalculation = VMConfig.allowHardenResourceCalculation(); + return snapshot; + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java index 75b11f4ab9d..2c7aa238033 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java @@ -1,7 +1,9 @@ package org.tron.common.runtime.vm; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -142,11 +144,14 @@ public void testLargeArrayLengthOverflow() { zeroOffset, largeLength, 0); boolean overflowCaught = false; + VMConfig.initAllowTvmOsaka(1); // cost3 self-dispatches; exercise the v3 (BigInteger) path try { EnergyCost.getVoteWitnessCost3(program); } catch (Program.OutOfMemoryException e) { // cost3 should detect memory overflow via checkMemorySize overflowCaught = true; + } finally { + VMConfig.initAllowTvmOsaka(0); } assertTrue("cost3 should throw memoryOverflow for huge array length", overflowCaught); } @@ -161,10 +166,13 @@ public void testLargeOffsetOverflow() { new DataWord(0), new DataWord(1), 0); boolean overflowCaught = false; + VMConfig.initAllowTvmOsaka(1); // cost3 self-dispatches; exercise the v3 (BigInteger) path try { EnergyCost.getVoteWitnessCost3(program); } catch (Program.OutOfMemoryException e) { overflowCaught = true; + } finally { + VMConfig.initAllowTvmOsaka(0); } assertTrue("cost3 should throw memoryOverflow for huge offset", overflowCaught); } @@ -239,4 +247,49 @@ public void testOperationRegistryWithOsaka() { VMConfig.initAllowTvmOsaka(0); } } + + @Test + public void testCost3FallsBackToCost2WhenOsakaOff() { + // cost3 self-dispatches: with osaka off it delegates to cost2, so a cost3 left in the shared + // jump table (e.g. read by a constant call whose view has osaka off) still charges v2 energy. + String maxHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + DataWord huge = new DataWord(maxHex); + DataWord zero = new DataWord(0); + + VMConfig.initAllowTvmOsaka(0); + long viaCost3 = + EnergyCost.getVoteWitnessCost3(mockProgram(zero, new DataWord(1), zero, huge, 0)); + long viaCost2 = + EnergyCost.getVoteWitnessCost2(mockProgram(zero, new DataWord(1), zero, huge, 0)); + assertEquals("cost3 must equal cost2 when osaka is off", viaCost2, viaCost3); + + // sanity: with osaka on, cost3 instead runs v3 and detects the overflow that cost2 wraps away. + VMConfig.initAllowTvmOsaka(1); + try { + EnergyCost.getVoteWitnessCost3(mockProgram(zero, new DataWord(1), zero, huge, 0)); + fail("cost3 with osaka on must overflow-throw on the huge length"); + } catch (Program.OutOfMemoryException expected) { + // expected + } finally { + VMConfig.initAllowTvmOsaka(0); + } + } + + @Test + public void testCost2FallsBackToLegacyWhenEnergyAdjustmentOff() { + // cost2 self-dispatches: with energyAdjustment off it delegates to the legacy cost. + VMConfig.initAllowEnergyAdjustment(0); + try { + long viaCost2 = EnergyCost.getVoteWitnessCost2(mockProgram(0, 2, 128, 2, 0)); + long viaLegacy = EnergyCost.getVoteWitnessCost(mockProgram(0, 2, 128, 2, 0)); + assertEquals("cost2 must equal legacy cost when energyAdjustment is off", + viaLegacy, viaCost2); + + // sanity: with energyAdjustment on, cost2 differs from the legacy cost for this input. + VMConfig.initAllowEnergyAdjustment(1); + assertNotEquals(viaLegacy, EnergyCost.getVoteWitnessCost2(mockProgram(0, 2, 128, 2, 0))); + } finally { + VMConfig.initAllowEnergyAdjustment(1); + } + } } diff --git a/framework/src/test/java/org/tron/common/utils/client/utils/Sha256Sm3Hash.java b/framework/src/test/java/org/tron/common/utils/client/utils/Sha256Sm3Hash.java index fde88385794..a034f9e816a 100644 --- a/framework/src/test/java/org/tron/common/utils/client/utils/Sha256Sm3Hash.java +++ b/framework/src/test/java/org/tron/common/utils/client/utils/Sha256Sm3Hash.java @@ -47,15 +47,6 @@ public class Sha256Sm3Hash implements Serializable, Comparable { private final byte[] bytes; private static boolean isEckey = true; - /* static { - Config config = Configuration.getByPath("config.conf"); // it is needs set to be a constant - Config config = "crypto.engine"; - if (config.hasPath("crypto.engine")) { - isEckey = config.getString("crypto.engine").equalsIgnoreCase("eckey"); - System.out.println("Sha256Sm3Hash getConfig isEckey: " + isEckey); - } - }*/ - public Sha256Sm3Hash(long num, byte[] hash) { byte[] rawHashBytes = this.generateBlockId(num, hash); Preconditions.checkArgument(rawHashBytes.length == LENGTH); diff --git a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java index 0a8fbac009c..00be867fd59 100644 --- a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java +++ b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java @@ -22,6 +22,7 @@ import org.tron.api.GrpcAPI.ShieldedTRC20TriggerContractParameters; import org.tron.api.GrpcAPI.SpendAuthSigParameters; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.PublicMethod; @@ -63,7 +64,7 @@ public class ShieldedTRC20BuilderTest extends BaseTest { private static final byte[] PUBLIC_TO_ADDRESS; static { - Args.setParam(new String[]{"--output-directory", dbPath()}, "config-test-mainnet.conf"); + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); SHIELDED_CONTRACT_ADDRESS = WalletClient.decodeFromBase58Check(SHIELDED_CONTRACT_ADDRESS_STR); DEFAULT_OVK = ByteArray .fromHexString("030c8c2bc59fb3eb8afb047a8ea4b028743d23e7d38c6fa30908358431e2314d"); diff --git a/framework/src/test/java/org/tron/core/WalletMockTest.java b/framework/src/test/java/org/tron/core/WalletMockTest.java index 3e0c1a4461d..2f4c08d8f9f 100644 --- a/framework/src/test/java/org/tron/core/WalletMockTest.java +++ b/framework/src/test/java/org/tron/core/WalletMockTest.java @@ -7,10 +7,12 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.google.common.cache.Cache; @@ -24,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -39,6 +42,7 @@ import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.client.WalletClient; +import org.tron.common.zksnark.JLibrustzcash; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.ContractCapsule; @@ -76,6 +80,7 @@ import org.tron.core.zen.address.ExpandedSpendingKey; import org.tron.core.zen.address.KeyIo; import org.tron.core.zen.address.PaymentAddress; +import org.tron.core.zen.note.Note; import org.tron.protos.Protocol; import org.tron.protos.contract.BalanceContract; import org.tron.protos.contract.ShieldContract; @@ -164,6 +169,54 @@ public void testCreateTransactionCapsuleWithoutValidateWithTimeout() } + @Test + public void testBroadcastTxInvalidSigLength() throws Exception { + Wallet wallet = new Wallet(); + TronNetDelegate tronNetDelegateMock = mock(TronNetDelegate.class); + Field field = wallet.getClass().getDeclaredField("tronNetDelegate"); + field.setAccessible(true); + field.set(wallet, tronNetDelegateMock); + + // signature shorter than 65 bytes → SIGERROR + Protocol.Transaction shortSig = Protocol.Transaction.newBuilder() + .addSignature(ByteString.copyFrom(new byte[64])) + .build(); + GrpcAPI.Return ret = wallet.broadcastTransaction(shortSig); + assertEquals(GrpcAPI.Return.response_code.SIGERROR, ret.getCode()); + + // signature longer than 68 bytes → SIGERROR + Protocol.Transaction longSig = Protocol.Transaction.newBuilder() + .addSignature(ByteString.copyFrom(new byte[69])) + .build(); + ret = wallet.broadcastTransaction(longSig); + assertEquals(GrpcAPI.Return.response_code.SIGERROR, ret.getCode()); + + // empty signature → SIGERROR + Protocol.Transaction emptySig = Protocol.Transaction.newBuilder() + .addSignature(ByteString.EMPTY) + .build(); + ret = wallet.broadcastTransaction(emptySig); + assertEquals(GrpcAPI.Return.response_code.SIGERROR, ret.getCode()); + + // tronNetDelegate must not be consulted because the request is rejected up front + Mockito.verify(tronNetDelegateMock, Mockito.never()).isBlockUnsolidified(); + + // 65-byte signature passes the length check and proceeds to downstream logic + when(tronNetDelegateMock.isBlockUnsolidified()).thenReturn(true); + Protocol.Transaction validSig = Protocol.Transaction.newBuilder() + .addSignature(ByteString.copyFrom(new byte[65])) + .build(); + ret = wallet.broadcastTransaction(validSig); + assertEquals(GrpcAPI.Return.response_code.BLOCK_UNSOLIDIFIED, ret.getCode()); + + // 68-byte signature (upper bound) also passes the length check + Protocol.Transaction paddedSig = Protocol.Transaction.newBuilder() + .addSignature(ByteString.copyFrom(new byte[68])) + .build(); + ret = wallet.broadcastTransaction(paddedSig); + assertEquals(GrpcAPI.Return.response_code.BLOCK_UNSOLIDIFIED, ret.getCode()); + } + @Test public void testBroadcastTransactionBlockUnsolidified() throws Exception { Wallet wallet = new Wallet(); @@ -1142,9 +1195,10 @@ public void testGetShieldedTRC20LogTypeReturnsCorrectInt() throws Exception { "MintNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])", "TransferNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])", "BurnNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])", - "TokenBurn(address,uint256,bytes32[3])" + "TokenBurn(address,uint256,bytes32[3])", + "NoteSpent(bytes32)" }; - int[] expectedTypes = {1, 2, 3, 4}; + int[] expectedTypes = {1, 2, 3, 4, 5}; for (int i = 0; i < eventSignatures.length; i++) { byte[] topicHash = Hash.sha3(ByteArray.fromString(eventSignatures[i])); @@ -1158,6 +1212,70 @@ public void testGetShieldedTRC20LogTypeReturnsCorrectInt() throws Exception { } } + @Test + public void scanShieldedTRC20NotesByIvkSkipsNoteSpentIndex() throws Exception { + final String SHIELDED_CONTRACT_ADDRESS_STR = "TGAmX5AqVUoXCf8MoHxbuhQPmhGfWTnEgA"; + byte[] contractAddress = WalletClient.decodeFromBase58Check(SHIELDED_CONTRACT_ADDRESS_STR); + byte[] addressWithoutPrefix = new byte[20]; + System.arraycopy(contractAddress, 1, addressWithoutPrefix, 0, 20); + + byte[] noteSpentTopic = Hash.sha3(ByteArray.fromString("NoteSpent(bytes32)")); + Protocol.TransactionInfo.Log noteSpentLog = Protocol.TransactionInfo.Log.newBuilder() + .setAddress(ByteString.copyFrom(addressWithoutPrefix)) + .addTopics(ByteString.copyFrom(noteSpentTopic)) + .setData(ByteString.copyFrom(new byte[32])) + .build(); + + byte[] transferTopic = Hash.sha3(ByteArray.fromString( + "TransferNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])")); + // getNoteTxFromLogListByIvk slices bytes 0..708; only `pos` (bytes 0..32) is read here. + byte[] transferData = new byte[708]; + Protocol.TransactionInfo.Log transferLog = Protocol.TransactionInfo.Log.newBuilder() + .setAddress(ByteString.copyFrom(addressWithoutPrefix)) + .addTopics(ByteString.copyFrom(transferTopic)) + .setData(ByteString.copyFrom(transferData)) + .build(); + + Protocol.TransactionInfo info = Protocol.TransactionInfo.newBuilder() + .addLog(noteSpentLog) + .addLog(transferLog) + .build(); + + Protocol.Block block = Protocol.Block.newBuilder() + .addTransactions(Protocol.Transaction.newBuilder().build()) + .build(); + GrpcAPI.BlockList blockList = GrpcAPI.BlockList.newBuilder().addBlock(block).build(); + + Wallet wallet = spy(new Wallet()); + doReturn(blockList).when(wallet).getBlocksByLimitNext(anyLong(), anyLong()); + doReturn(info).when(wallet).getTransactionInfoById(any()); + + // Bypass the real ZK crypto: return a valid note and a deterministic payment address + // so the scanner reaches the index-assignment branch. + Note fakeNote = new Note(new DiversifierT(), new byte[32], 100L, + new byte[32], new byte[512]); + boolean prevAllow = CommonParameter.getInstance().isAllowShieldedTransactionApi(); + CommonParameter.getInstance().setAllowShieldedTransactionApi(true); + try (MockedStatic noteMock = mockStatic(Note.class); + MockedStatic rustMock = mockStatic(JLibrustzcash.class); + MockedStatic keyIoMock = mockStatic(KeyIo.class)) { + noteMock.when(() -> Note.decrypt(any(byte[].class), any(byte[].class), + any(byte[].class), any(byte[].class))).thenReturn(Optional.of(fakeNote)); + rustMock.when(() -> JLibrustzcash.librustzcashIvkToPkd(any())).thenReturn(true); + keyIoMock.when(() -> KeyIo.encodePaymentAddress(any())).thenReturn("zaddrFake"); + + byte[] ivk = new byte[32]; + GrpcAPI.DecryptNotesTRC20 result = wallet.scanShieldedTRC20NotesByIvk( + 0L, 1L, contractAddress, ivk, new byte[0], new byte[0]); + + assertEquals(1, result.getNoteTxsCount()); + assertEquals("TransferNewLeaf must get index 0; NoteSpent must not advance the counter", + 0L, result.getNoteTxs(0).getIndex()); + } finally { + CommonParameter.getInstance().setAllowShieldedTransactionApi(prevAllow); + } + } + @Test public void testBuildShieldedTRC20InputWithAK() throws ZksnarkException { Wallet wallet = new Wallet(); diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index 0df8d6cdc2c..9dbab338b67 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -1440,5 +1440,133 @@ public void testGetSolidBlock() { Block block = wallet.getSolidBlock(); assertEquals(block2, block); } + + @Test + public void testApprovedListSigBound() { + ECKey ecKey = new ECKey(Utils.getRandom()); + AccountCapsule owner = new AccountCapsule( + ByteString.copyFromUtf8("approved-owner"), + ByteString.copyFrom(ecKey.getAddress()), + Protocol.AccountType.Normal, + initBalance); + chainBaseManager.getAccountStore().put(ecKey.getAddress(), owner); + // Default owner permission: a single key with weight 1, so keysCount == 1. + int keysCount = owner.getPermissionById(0).getKeysCount(); + assertEquals(1, keysCount); + + Transaction unsigned = Transaction.newBuilder().setRawData( + Transaction.raw.newBuilder().addContract( + Contract.newBuilder().setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder().setAmount(1) + .setOwnerAddress(ByteString.copyFrom(ecKey.getAddress())) + .setToAddress(ByteString.copyFrom( + ByteArray.fromHexString(RECEIVER_ADDRESS))) + .build())).build()).build()).build(); + + // One valid 65-byte [r][s][recId] signature by the owner. + TransactionCapsule capsule = new TransactionCapsule(unsigned); + capsule.sign(ecKey.getPrivKeyBytes()); + ByteString oneSig = capsule.getInstance().getSignature(0); + + // Within keysCount: the single valid signature is recovered, result is SUCCESS. + GrpcAPI.TransactionApprovedList okList = wallet.getTransactionApprovedList( + unsigned.toBuilder().addSignature(oneSig).build()); + assertEquals(GrpcAPI.TransactionApprovedList.Result.response_code.SUCCESS, + okList.getResult().getCode()); + assertEquals(1, okList.getApprovedListCount()); + + // More signatures than keysCount: checkWeight rejects before recovering any of them, + // so the unbounded ecrecover loop can no longer be triggered. + Transaction.Builder overLimit = unsigned.toBuilder(); + for (int i = 0; i < keysCount + 1; i++) { + overLimit.addSignature(oneSig); + } + GrpcAPI.TransactionApprovedList rejected = + wallet.getTransactionApprovedList(overLimit.build()); + assertEquals(GrpcAPI.TransactionApprovedList.Result.response_code.OTHER_ERROR, + rejected.getResult().getCode()); + assertEquals(0, rejected.getApprovedListCount()); + Assert.assertFalse(rejected.getResult().getMessage().isEmpty()); + } + + @Test + public void testApprovedListSigTruncate() { + ECKey ecKey = new ECKey(Utils.getRandom()); + AccountCapsule owner = new AccountCapsule( + ByteString.copyFromUtf8("approved-owner-trunc"), + ByteString.copyFrom(ecKey.getAddress()), + Protocol.AccountType.Normal, + initBalance); + chainBaseManager.getAccountStore().put(ecKey.getAddress(), owner); + + Transaction unsigned = Transaction.newBuilder().setRawData( + Transaction.raw.newBuilder().addContract( + Contract.newBuilder().setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder().setAmount(1) + .setOwnerAddress(ByteString.copyFrom(ecKey.getAddress())) + .setToAddress(ByteString.copyFrom( + ByteArray.fromHexString(RECEIVER_ADDRESS))) + .build())).build()).build()).build(); + + TransactionCapsule capsule = new TransactionCapsule(unsigned); + capsule.sign(ecKey.getPrivKeyBytes()); + ByteString validSig = capsule.getInstance().getSignature(0); + assertEquals(65, validSig.size()); + + // Pad the 65-byte signature with trailing junk bytes. + ByteString oversized = validSig.concat( + ByteString.copyFrom(new byte[] {1, 2, 3, 4, 5})); + assertEquals(70, oversized.size()); + + GrpcAPI.TransactionApprovedList reply = wallet.getTransactionApprovedList( + unsigned.toBuilder().addSignature(oversized).build()); + + // Recovery still succeeds and resolves the owner. + assertEquals(GrpcAPI.TransactionApprovedList.Result.response_code.SUCCESS, + reply.getResult().getCode()); + assertEquals(1, reply.getApprovedListCount()); + // The echoed-back transaction has the signature truncated to 65 bytes. + Transaction echoed = reply.getTransaction().getTransaction(); + assertEquals(1, echoed.getSignatureCount()); + assertEquals(65, echoed.getSignature(0).size()); + assertEquals(validSig, echoed.getSignature(0)); + } + + @Test + public void testApprovedListTooManySigs() { + ECKey ecKey = new ECKey(Utils.getRandom()); + AccountCapsule owner = new AccountCapsule( + ByteString.copyFromUtf8("total-sign-num-owner"), + ByteString.copyFrom(ecKey.getAddress()), + Protocol.AccountType.Normal, + initBalance); + chainBaseManager.getAccountStore().put(ecKey.getAddress(), owner); + + Transaction unsigned = Transaction.newBuilder().setRawData( + Transaction.raw.newBuilder().addContract( + Contract.newBuilder().setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder().setAmount(1) + .setOwnerAddress(ByteString.copyFrom(ecKey.getAddress())) + .setToAddress(ByteString.copyFrom( + ByteArray.fromHexString(RECEIVER_ADDRESS))) + .build())).build()).build()).build(); + + TransactionCapsule capsule = new TransactionCapsule(unsigned); + capsule.sign(ecKey.getPrivKeyBytes()); + ByteString oneSig = capsule.getInstance().getSignature(0); + + int totalSignNum = chainBaseManager.getDynamicPropertiesStore().getTotalSignNum(); + Transaction.Builder overLimit = unsigned.toBuilder(); + for (int i = 0; i < totalSignNum + 1; i++) { + overLimit.addSignature(oneSig); + } + + GrpcAPI.TransactionApprovedList rejected = + wallet.getTransactionApprovedList(overLimit.build()); + assertEquals(GrpcAPI.TransactionApprovedList.Result.response_code.OTHER_ERROR, + rejected.getResult().getCode()); + Assert.assertTrue(rejected.getResult().getMessage().contains("too many signatures")); + assertEquals(0, rejected.getApprovedListCount()); + } } diff --git a/framework/src/test/java/org/tron/core/actuator/utils/TransactionUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/TransactionUtilTest.java index 15842bfa2c8..54e611e0aac 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/TransactionUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/TransactionUtilTest.java @@ -13,18 +13,23 @@ import static org.tron.core.utils.TransactionUtil.validAssetName; import static org.tron.core.utils.TransactionUtil.validTokenAbbrName; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.tron.api.GrpcAPI.TransactionSignWeight; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Utils; import org.tron.core.ChainBaseManager; import org.tron.core.Constant; import org.tron.core.Wallet; @@ -36,14 +41,19 @@ import org.tron.protos.Protocol; import org.tron.protos.Protocol.AccountType; import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.BalanceContract.DelegateResourceContract; +import org.tron.protos.contract.BalanceContract.TransferContract; @Slf4j(topic = "capsule") public class TransactionUtilTest extends BaseTest { private static String OWNER_ADDRESS; + @Resource + private TransactionUtil transactionUtil; + /** * Init . */ @@ -452,4 +462,84 @@ public void testConcurrentToString() throws InterruptedException { } Assert.assertTrue(true); } + + @Test + public void testSignWeightSigTruncate() { + ECKey ecKey = new ECKey(Utils.getRandom()); + AccountCapsule owner = new AccountCapsule( + ByteString.copyFromUtf8("sign-weight-owner"), + ByteString.copyFrom(ecKey.getAddress()), + AccountType.Normal, + 10_000_000_000L); + chainBaseManager.getAccountStore().put(ecKey.getAddress(), owner); + + Transaction unsigned = Transaction.newBuilder().setRawData( + Transaction.raw.newBuilder().addContract( + Contract.newBuilder().setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder().setAmount(1) + .setOwnerAddress(ByteString.copyFrom(ecKey.getAddress())) + .setToAddress(ByteString.copyFrom( + ByteArray.fromHexString(OWNER_ADDRESS))) + .build())).build()).build()).build(); + + TransactionCapsule capsule = new TransactionCapsule(unsigned); + capsule.sign(ecKey.getPrivKeyBytes()); + ByteString validSig = capsule.getInstance().getSignature(0); + assertEquals(65, validSig.size()); + + // Pad the 65-byte signature with trailing junk bytes. + ByteString oversized = validSig.concat( + ByteString.copyFrom(new byte[] {1, 2, 3, 4, 5})); + assertEquals(70, oversized.size()); + + TransactionSignWeight reply = transactionUtil.getTransactionSignWeight( + unsigned.toBuilder().addSignature(oversized).build()); + + // Recovery still resolves the owner (weight reached the default threshold). + assertEquals(TransactionSignWeight.Result.response_code.ENOUGH_PERMISSION, + reply.getResult().getCode()); + assertEquals(1, reply.getApprovedListCount()); + // The echoed-back transaction has the signature truncated to 65 bytes. + Transaction echoed = reply.getTransaction().getTransaction(); + assertEquals(1, echoed.getSignatureCount()); + assertEquals(65, echoed.getSignature(0).size()); + assertEquals(validSig, echoed.getSignature(0)); + } + + @Test + public void testSignWeightTooManySigs() { + ECKey ecKey = new ECKey(Utils.getRandom()); + AccountCapsule owner = new AccountCapsule( + ByteString.copyFromUtf8("sign-weight-total-num"), + ByteString.copyFrom(ecKey.getAddress()), + AccountType.Normal, + 10_000_000_000L); + chainBaseManager.getAccountStore().put(ecKey.getAddress(), owner); + + Transaction unsigned = Transaction.newBuilder().setRawData( + Transaction.raw.newBuilder().addContract( + Contract.newBuilder().setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder().setAmount(1) + .setOwnerAddress(ByteString.copyFrom(ecKey.getAddress())) + .setToAddress(ByteString.copyFrom( + ByteArray.fromHexString(OWNER_ADDRESS))) + .build())).build()).build()).build(); + + TransactionCapsule capsule = new TransactionCapsule(unsigned); + capsule.sign(ecKey.getPrivKeyBytes()); + ByteString oneSig = capsule.getInstance().getSignature(0); + + int totalSignNum = chainBaseManager.getDynamicPropertiesStore().getTotalSignNum(); + Transaction.Builder overLimit = unsigned.toBuilder(); + for (int i = 0; i < totalSignNum + 1; i++) { + overLimit.addSignature(oneSig); + } + + TransactionSignWeight reply = transactionUtil.getTransactionSignWeight( + overLimit.build()); + assertEquals(TransactionSignWeight.Result.response_code.OTHER_ERROR, + reply.getResult().getCode()); + Assert.assertTrue(reply.getResult().getMessage().contains("too many signatures")); + assertEquals(0, reply.getApprovedListCount()); + } } diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java index ca0844c2c16..b258fbf99a1 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java @@ -1,5 +1,8 @@ package org.tron.core.capsule; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.google.protobuf.ByteString; import java.io.IOException; import java.util.ArrayList; @@ -21,6 +24,11 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.BadBlockException; import org.tron.core.exception.BadItemException; +import org.tron.core.exception.ValidateSignatureException; +import org.tron.core.store.AccountStore; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.BalanceContract.TransferContract; @@ -180,6 +188,95 @@ public void testGetTimeStamp() { Assert.assertEquals(1234L, blockCapsule0.getTimeStamp()); } + /** + * Pin the contract that switchFork's signature recheck relies on: + * when the recovered signer address does not match the witness address, + * validateSignature returns false (no exception). switchFork uses the + * boolean return to decide whether to throw, so this contract is what + * makes the fix work for "wrong signer" attacks. + */ + @Test + public void testValidateSignatureReturnsFalseWhenSignerMismatch() throws Exception { + String signerKey = PublicMethod.getRandomPrivateKey(); + String witnessKey = PublicMethod.getRandomPrivateKey(); + byte[] witnessAddress = PublicMethod.getAddressByteByPrivateKey(witnessKey); + + BlockCapsule block = new BlockCapsule(2, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))), + 4321, + ByteString.copyFrom(witnessAddress)); + block.sign(ByteArray.fromHexString(signerKey)); + + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getAllowMultiSign()).thenReturn(0L); + AccountStore accountStore = mock(AccountStore.class); + + Assert.assertFalse(block.validateSignature(dps, accountStore)); + } + + /** + * Same key path under the happy case: when signer == witness, validateSignature + * returns true. Guards against any future refactor that accidentally inverts + * the comparison or strips the witness check. + */ + @Test + public void testValidateSignatureReturnsTrueWhenSignerMatches() throws Exception { + String key = PublicMethod.getRandomPrivateKey(); + byte[] witnessAddress = PublicMethod.getAddressByteByPrivateKey(key); + + BlockCapsule block = new BlockCapsule(3, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))), + 5678, + ByteString.copyFrom(witnessAddress)); + block.sign(ByteArray.fromHexString(key)); + + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getAllowMultiSign()).thenReturn(0L); + AccountStore accountStore = mock(AccountStore.class); + + Assert.assertTrue(block.validateSignature(dps, accountStore)); + } + + /** + * The other failure mode switchFork must handle: signature bytes are + * malformed (cannot recover a public key). validateSignature wraps the + * underlying SignatureException as ValidateSignatureException, which the + * existing catch block in switchFork already handles. + */ + @Test(expected = ValidateSignatureException.class) + public void testValidateSignatureThrowsForMalformedSignature() throws Exception { + byte[] witnessAddress = PublicMethod.getAddressByteByPrivateKey( + PublicMethod.getRandomPrivateKey()); + + // 65-byte signature with valid length but garbage content — passes Rsv parsing + // but fails ECDSA recovery, surfacing SignatureException → ValidateSignatureException. + byte[] garbageSigBytes = new byte[65]; + Arrays.fill(garbageSigBytes, (byte) 0xAB); + ByteString garbageSig = ByteString.copyFrom(garbageSigBytes); + + BlockHeader.raw rawData = BlockHeader.raw.newBuilder() + .setNumber(4) + .setTimestamp(1111) + .setParentHash(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))) + .setWitnessAddress(ByteString.copyFrom(witnessAddress)) + .build(); + BlockHeader header = BlockHeader.newBuilder() + .setRawData(rawData) + .setWitnessSignature(garbageSig) + .build(); + Block proto = Block.newBuilder().setBlockHeader(header).build(); + BlockCapsule block = new BlockCapsule(proto); + + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getAllowMultiSign()).thenReturn(0L); + AccountStore accountStore = mock(AccountStore.class); + + block.validateSignature(dps, accountStore); + } + @Test public void testConcurrentToString() throws InterruptedException { List threadList = new ArrayList<>(); 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/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 4b6b7ad0a7a..076a8ab5387 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -17,7 +17,6 @@ import com.google.common.collect.Lists; import com.typesafe.config.Config; -import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigFactory; import io.grpc.internal.GrpcUtil; import io.grpc.netty.NettyServerBuilder; @@ -39,6 +38,7 @@ import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; +import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.TronError; @Slf4j @@ -50,8 +50,7 @@ public class ArgsTest { @Test public void get() { - Args.setParam(new String[] {"-c", TestConstants.TEST_CONF, "--keystore-factory"}, - TestConstants.NET_CONF); + Args.setParam(new String[] {"--keystore-factory"}, TestConstants.TEST_CONF); CommonParameter parameter = Args.getInstance(); @@ -73,7 +72,7 @@ public void get() { Assert.assertEquals("database", parameter.getStorage().getDbDirectory()); - Assert.assertEquals(11, parameter.getSeedNode().getAddressList().size()); + Assert.assertEquals(0, parameter.getSeedNode().getAddressList().size()); GenesisBlock genesisBlock = parameter.getGenesisBlock(); @@ -147,12 +146,6 @@ public void testIpFromLibP2p() Assert.assertNotEquals(configuredExternalIp, parameter.getNodeExternalIp()); } - @Test - public void testOldRewardOpt() { - thrown.expect(IllegalArgumentException.class); - Args.setParam(new String[] {"-c", "args-test.conf"}, TestConstants.NET_CONF); - } - @Test public void testInitService() { Map storage = new HashMap<>(); @@ -303,8 +296,6 @@ public void testCliOverridesStorageConfig() { "--storage-db-directory", "cli-db-dir", "--storage-db-engine", "ROCKSDB", "--storage-db-synchronous", "true", - "--storage-index-directory", "cli-index-dir", - "--storage-index-switch", "cli-index-switch", "--storage-transactionHistory-switch", "off", "--contract-parse-enable", "false" }, TestConstants.TEST_CONF); @@ -314,8 +305,6 @@ public void testCliOverridesStorageConfig() { Assert.assertEquals("cli-db-dir", parameter.getStorage().getDbDirectory()); Assert.assertEquals("ROCKSDB", parameter.getStorage().getDbEngine()); Assert.assertTrue(parameter.getStorage().isDbSync()); - Assert.assertEquals("cli-index-dir", parameter.getStorage().getIndexDirectory()); - Assert.assertEquals("cli-index-switch", parameter.getStorage().getIndexSwitch()); Assert.assertEquals("off", parameter.getStorage().getTransactionHistorySwitch()); Assert.assertFalse(parameter.getStorage().isContractParseSwitch()); @@ -344,6 +333,21 @@ public void testCliEsOverridesConfig() { Args.clearParam(); } + /** + * Regression: when --es is the sole source of event.subscribe.enable=true + * (config has it disabled), eventPluginConfig must be built. + * Previously applyEventConfig() ran before applyCLIParams() and returned + * early (both flags false), leaving eventPluginConfig=null; Manager then + * called EventPluginLoader.start(null) and threw "Failed to load eventPlugin." + */ + @Test + public void testCliEsBuildsEventPluginConfig() { + Args.setParam(new String[] {"--es"}, TestConstants.TEST_CONF); + Assert.assertTrue(Args.getInstance().isEventSubscribe()); + Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); + Args.clearParam(); + } + /** * Verify that config file storage values are applied when no CLI override is present. * @@ -366,12 +370,9 @@ public void testConfigStorageDefaults() { } // =========================================================================== - // Boundary tests for clamps applied in Args.java bridge code (not in - // bean postProcess()). + // Boundary tests for node.fetchBlock.timeout clamping. // - // fetchBlockTimeout is read from NodeConfig but clamped in Args.applyNodeConfig - // to range [100, 1000]. Pin this clamp here so any future refactor that moves - // it (e.g. into NodeConfig.postProcess()) preserves the behavior. + // The clamp to [100, 1000] is applied in NodeConfig.postProcess(). // =========================================================================== @Test @@ -398,35 +399,6 @@ public void testFetchBlockTimeoutClampedAboveMax() { Args.clearParam(); } - - @Test - public void testHttpJsonParseConstraints() { - Map override = new HashMap<>(); - override.put("storage.db.directory", "database"); - Config config = ConfigFactory.parseMap(override) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - - Assert.assertEquals(100, Args.getInstance().getMaxNestingDepth()); - Assert.assertEquals(100_000, Args.getInstance().getMaxTokenCount()); - Args.clearParam(); - } - - @Test - public void testHttpJsonParseConstraintsApplied() { - Map override = new HashMap<>(); - override.put("storage.db.directory", "database"); - override.put("node.http.maxNestingDepth", "42"); - override.put("node.http.maxTokenCount", "12345"); - Config config = ConfigFactory.parseMap(override) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - - Assert.assertEquals(42, Args.getInstance().getMaxNestingDepth()); - Assert.assertEquals(12345, Args.getInstance().getMaxTokenCount()); - Args.clearParam(); - } - @Test public void testFetchBlockTimeoutInRangeUnchanged() { Map override = new HashMap<>(); @@ -454,6 +426,7 @@ public void testEventConfigDisabledSkipsEpcAndFilter() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); Assert.assertNull(Args.getInstance().getEventPluginConfig()); Assert.assertNull(Args.getInstance().getEventFilter()); Args.clearParam(); @@ -467,6 +440,7 @@ public void testEventConfigEnabledBuildsEpcAndFilter() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); Assert.assertNotNull(Args.getInstance().getEventFilter()); Args.clearParam(); @@ -481,6 +455,7 @@ public void testEventConfigEnabledWithInvalidFromBlockLeavesFilterNull() { Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); + Args.applyEventConfig(); // epc still built; filter rejected Assert.assertNotNull(Args.getInstance().getEventPluginConfig()); Assert.assertNull(Args.getInstance().getEventFilter()); @@ -497,79 +472,14 @@ public void testAllowShieldedTransactionApiDefault() { } @Test - public void testMaxMessageSizeHumanReadable() { + public void testMaxMessageSizePureNumber() { Map configMap = new HashMap<>(); configMap.put("storage.db.directory", "database"); - // --- KB tier: binary (k/K/Ki/KiB = 1024) vs SI (kB = 1000) --- - configMap.put("node.rpc.maxMessageSize", "512k"); - configMap.put("node.http.maxMessageSize", "512K"); - configMap.put("node.jsonrpc.maxMessageSize", "512kB"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - Assert.assertEquals(512 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(512 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(512 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - - configMap.put("node.rpc.maxMessageSize", "256Ki"); - configMap.put("node.http.maxMessageSize", "256KiB"); - configMap.put("node.jsonrpc.maxMessageSize", "256kB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - Assert.assertEquals(256 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(256 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(256 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - - // --- MB tier: binary (m/M/Mi/MiB = 1024*1024) vs SI (MB = 1000*1000) --- - configMap.put("node.rpc.maxMessageSize", "4m"); - configMap.put("node.http.maxMessageSize", "8M"); - configMap.put("node.jsonrpc.maxMessageSize", "2MB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(8 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(2 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - - configMap.put("node.rpc.maxMessageSize", "4Mi"); - configMap.put("node.http.maxMessageSize", "4MiB"); - configMap.put("node.jsonrpc.maxMessageSize", "4MB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(4 * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - - // --- GB tier: binary (g/G/Gi/GiB) vs SI (GB) --- - // All three paths are int-bounded; values up to Integer.MAX_VALUE are accepted. - configMap.put("node.rpc.maxMessageSize", "4m"); - configMap.put("node.http.maxMessageSize", "1g"); - configMap.put("node.jsonrpc.maxMessageSize", "1GB"); - config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - Args.applyConfigParams(config); - Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getMaxMessageSize()); - Assert.assertEquals(1024L * 1024 * 1024, Args.getInstance().getHttpMaxMessageSize()); - Assert.assertEquals(1000L * 1000 * 1000, Args.getInstance().getJsonRpcMaxMessageSize()); - Args.clearParam(); - - // --- raw integer (backward compatible): treated as bytes --- configMap.put("node.rpc.maxMessageSize", "4194304"); configMap.put("node.http.maxMessageSize", "4194304"); configMap.put("node.jsonrpc.maxMessageSize", "4194304"); - config = ConfigFactory.defaultOverrides() + Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)) .withFallback(ConfigFactory.defaultReference()); Args.applyConfigParams(config); @@ -578,7 +488,6 @@ public void testMaxMessageSizeHumanReadable() { Assert.assertEquals(4 * 1024 * 1024, Args.getInstance().getJsonRpcMaxMessageSize()); Args.clearParam(); - // --- zero is allowed --- configMap.put("node.rpc.maxMessageSize", "0"); configMap.put("node.http.maxMessageSize", "0"); configMap.put("node.jsonrpc.maxMessageSize", "0"); @@ -593,82 +502,39 @@ public void testMaxMessageSizeHumanReadable() { } @Test - public void testRpcMaxMessageSizeExceedsIntMax() { - Map configMap = new HashMap<>(); - configMap.put("storage.db.directory", "database"); - configMap.put("node.rpc.maxMessageSize", "3g"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - TronError e = Assert.assertThrows(TronError.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue(e.getMessage().contains("node.rpc.maxMessageSize must be non-negative")); - } - - @Test - public void testHttpMaxMessageSizeExceedsIntMax() { - Map configMap = new HashMap<>(); - configMap.put("storage.db.directory", "database"); - configMap.put("node.http.maxMessageSize", "2Gi"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - TronError e = Assert.assertThrows(TronError.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue(e.getMessage().contains("node.http.maxMessageSize must be non-negative")); - } - - @Test - public void testJsonRpcMaxMessageSizeExceedsIntMax() { - Map configMap = new HashMap<>(); - configMap.put("storage.db.directory", "database"); - configMap.put("node.jsonrpc.maxMessageSize", "2Gi"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - TronError e = Assert.assertThrows(TronError.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue( - e.getMessage().contains("node.jsonrpc.maxMessageSize must be non-negative")); - } - - @Test - public void testMaxMessageSizeNegativeValue() { - Map configMap = new HashMap<>(); - configMap.put("storage.db.directory", "database"); - configMap.put("node.rpc.maxMessageSize", "-4m"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue(e.getMessage().contains("negative")); - } - - @Test - public void testMaxMessageSizeInvalidUnit() { - Map configMap = new HashMap<>(); - configMap.put("storage.db.directory", "database"); - configMap.put("node.rpc.maxMessageSize", "4x"); - Config config = ConfigFactory.defaultOverrides() - .withFallback(ConfigFactory.parseMap(configMap)) - .withFallback(ConfigFactory.defaultReference()); - ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue(e.getMessage().contains("Could not parse size-in-bytes unit")); + public void testMaxMessageSizeNegativeValueRejected() { + // Negative maxMessageSize must be rejected at startup which threw TronError(PARAMETER_INIT) + // for negative values). + for (String key : new String[]{ + "node.rpc.maxMessageSize", "node.http.maxMessageSize", "node.jsonrpc.maxMessageSize"}) { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put(key, "-1"); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)) + .withFallback(ConfigFactory.defaultReference()); + TronError e = Assert.assertThrows(TronError.class, () -> Args.applyConfigParams(config)); + Assert.assertEquals(TronError.ErrCode.PARAMETER_INIT, e.getErrCode()); + Args.clearParam(); + } } @Test - public void testMaxMessageSizeNonNumeric() { - Map configMap = new HashMap<>(); + public void testRpcMaxMessageSizeExceedsIntMax() { + // HOCON's Config.getInt() throws when a numeric value exceeds int range. + // This documents the failure mode for node.rpc.maxMessageSize (int field). + Map configMap = new HashMap<>(); configMap.put("storage.db.directory", "database"); - configMap.put("node.http.maxMessageSize", "abc"); + configMap.put("node.rpc.maxMessageSize", (long) Integer.MAX_VALUE + 1); Config config = ConfigFactory.defaultOverrides() .withFallback(ConfigFactory.parseMap(configMap)) .withFallback(ConfigFactory.defaultReference()); - ConfigException.BadValue e = Assert.assertThrows(ConfigException.BadValue.class, - () -> Args.applyConfigParams(config)); - Assert.assertTrue(e.getMessage().contains("No number in size-in-bytes value")); + try { + Args.applyConfigParams(config); + Assert.fail("Expected RuntimeException for maxMessageSize > Integer.MAX_VALUE"); + } catch (RuntimeException e) { + // ConfigBeanFactory/HOCON throws when binding a long out of int range + } } // ===== checkBackupMembers() tests ===== diff --git a/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java b/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java index 83a65926446..1b30518c7e3 100644 --- a/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java +++ b/framework/src/test/java/org/tron/core/config/args/LocalWitnessTest.java @@ -177,10 +177,11 @@ public void testConstructor() { public void testLocalWitnessConfig() throws IOException { Args.setParam( new String[]{"--output-directory", temporaryFolder.newFolder().toString(), "-w", "--debug"}, - "config-localtest.conf"); + TestConstants.SHIELD_CONF); LocalWitnesses witness = Args.getLocalWitnesses(); Assert.assertNotNull(witness.getPrivateKey()); Assert.assertNotNull(witness.getWitnessAccountAddress()); + Args.clearParam(); } @Test @@ -191,5 +192,6 @@ public void testNullLocalWitnessConfig() throws IOException { LocalWitnesses witness = Args.getLocalWitnesses(); Assert.assertNull(witness.getPrivateKey()); Assert.assertNull(witness.getWitnessAccountAddress()); + Args.clearParam(); } } diff --git a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java b/framework/src/test/java/org/tron/core/config/args/OverlayTest.java deleted file mode 100644 index 1b7045c5b21..00000000000 --- a/framework/src/test/java/org/tron/core/config/args/OverlayTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * java-tron is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * java-tron is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.tron.core.config.args; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class OverlayTest { - - private Overlay overlay = new Overlay(); - - @Before - public void setOverlay() { - overlay.setPort(8080); - } - - @Test(expected = IllegalArgumentException.class) - public void whenSetOutOfBoundsPort() { - overlay.setPort(-1); - } - - @Test - public void getOverlay() { - Assert.assertEquals(8080, overlay.getPort()); - } -} diff --git a/framework/src/test/java/org/tron/core/config/args/StorageTest.java b/framework/src/test/java/org/tron/core/config/args/StorageTest.java index eb349a2d146..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 @@ -15,22 +15,49 @@ package org.tron.core.config.args; +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; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.utils.FileUtil; import org.tron.common.utils.StorageUtils; public class StorageTest { - private static Storage storage; + private static final Storage storage; static { - Args.setParam(new String[]{}, "config-test-storagetest.conf"); + Args.setParam(new String[]{}, TestConstants.TEST_CONF); storage = Args.getInstance().getStorage(); + setupStorage(); + } + + private static void setupStorage() { + Config cfg = ConfigFactory.parseString( + "storage.default.maxOpenFiles = 50\n" + + "storage.defaultM.maxOpenFiles = 500\n" + + "storage.defaultL.maxOpenFiles = 1000\n" + + "storage.properties = [\n" + + " { name = account, path = storage_directory_test,\n" + + " blockSize = 4096, writeBufferSize = 10485760, cacheSize = 10485760,\n" + + " maxOpenFiles = 100 },\n" + + " { name = \"account-index\", path = storage_directory_test,\n" + + " blockSize = 4096, writeBufferSize = 10485760, cacheSize = 10485760,\n" + + " maxOpenFiles = 100 },\n" + + " { name = test_name, path = test_path,\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); + storage.setDefaultDbOptions(sc); + storage.setPropertyMapFromBean(sc.getProperties()); } @AfterClass @@ -42,7 +69,6 @@ public static void cleanup() { @Test public void getDirectory() { Assert.assertEquals("database", storage.getDbDirectory()); - Assert.assertEquals("index", storage.getIndexDirectory()); } @Test @@ -55,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()); @@ -97,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/db/AccountIndexStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountIndexStoreTest.java index 4971132b8c5..1ae1ab4b029 100755 --- a/framework/src/test/java/org/tron/core/db/AccountIndexStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountIndexStoreTest.java @@ -16,7 +16,6 @@ public class AccountIndexStoreTest extends BaseTest { private static String dbDirectory = "db_AccountIndexStore_test"; - private static String indexDirectory = "index_AccountIndexStore_test"; @Resource private AccountIndexStore accountIndexStore; private static byte[] address = TransactionStoreTest.randomBytes(32); @@ -26,8 +25,7 @@ public class AccountIndexStoreTest extends BaseTest { Args.setParam( new String[]{ "--output-directory", dbPath(), - "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory + "--storage-db-directory", dbDirectory }, TestConstants.TEST_CONF ); diff --git a/framework/src/test/java/org/tron/core/db/AccountStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountStoreTest.java index 2fae33870cb..003c3fe4ab3 100755 --- a/framework/src/test/java/org/tron/core/db/AccountStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountStoreTest.java @@ -33,7 +33,6 @@ public class AccountStoreTest extends BaseTest { private static final byte[] data = TransactionStoreTest.randomBytes(32); private static String dbDirectory = "db_AccountStore_test"; - private static String indexDirectory = "index_AccountStore_test"; @Resource private AccountStore accountStore; @Resource @@ -48,8 +47,7 @@ public class AccountStoreTest extends BaseTest { Args.setParam( new String[]{ "--output-directory", dbPath(), - "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory + "--storage-db-directory", dbDirectory }, TestConstants.TEST_CONF ); diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java index 186d897effa..be5a012c852 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -256,14 +256,18 @@ public void deploySkipsWhenForeignContractPresent() { * SR / validator parity: the producer's {@code generateBlock} simulation * loop and the validator's {@code processBlock} apply loop must see the * same storage state when transactions hit {@code HISTORY_STORAGE_ADDRESS}. - * That requires {@link HistoryBlockHashUtil#write} to run before the tx - * loop on both paths. {@code processBlock} writes at line 1858; this test - * pins the matching write inside {@code generateBlock}. + * Both paths run {@link HistoryBlockHashUtil#write} before their tx loop: + * {@code processBlock} after its {@code validBlock} guard, and + * {@code generateBlock} after the witness-permission guard, so a failed + * permission check never writes the parent hash. * - *

Spy {@code accountStateCallBack.preExecute} — called between the - * write and the tx loop on both paths — and snapshot the slot from inside - * the revoking session. Pre-fix the slot is empty (write never ran); - * post-fix it holds the parent block hash. + *

In {@code generateBlock} {@code preExecute} runs ahead of the write + * (it precedes the guard), so this spies + * {@code accountStateCallBack.executeGenerateFinish} — the last callback + * before {@code session.reset()} — and snapshots the slot from inside the + * revoking session. With no pending transactions the tx loop is a no-op, so + * reaching this callback means the write already ran: if it were dropped the + * slot would be empty; instead it holds the parent block hash. */ @Test public void generateBlockWritesParentHashBeforeTxLoop() throws Exception { @@ -286,7 +290,7 @@ public void generateBlockWritesParentHashBeforeTxLoop() throws Exception { chainBaseManager.getStorageRowStore()); captured.set(st.getValue(new DataWord(expectedSlot))); return inv.callRealMethod(); - }).when(spy).preExecute(Mockito.any(BlockCapsule.class)); + }).when(spy).executeGenerateFinish(); cbField.set(dbManager, spy); try { @@ -303,10 +307,11 @@ public void generateBlockWritesParentHashBeforeTxLoop() throws Exception { } assertNotNull( - "preExecute fired with an empty slot — write() must run before preExecute", + "executeGenerateFinish fired with an empty slot — " + + "write() must run during block generation", captured.get()); assertArrayEquals( - "slot must hold the parent block hash before the tx loop runs", + "slot must hold the parent block hash by the time generation finishes", expectedParentHash, captured.get().getData()); } @@ -352,8 +357,8 @@ public void deployCreatesCodeContractAndAccount() { assertTrue(chainBaseManager.getAccountStore().has(addr)); AccountCapsule account = chainBaseManager.getAccountStore().get(addr); - assertEquals(HistoryBlockHashUtil.HISTORY_STORAGE_NAME, - account.getAccountName().toStringUtf8()); + assertEquals("accountName must remain unset to mirror CREATE2-created accounts", + ByteString.EMPTY, account.getAccountName()); assertEquals(Protocol.AccountType.Contract, account.getType()); assertTrue("install marker must flip after a successful deploy", chainBaseManager.getDynamicPropertiesStore().isBlockHashHistoryInstalled()); diff --git a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java index e3de0441c97..946bef022d2 100644 --- a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java @@ -5,12 +5,14 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.protobuf.Any; @@ -22,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import lombok.SneakyThrows; @@ -42,6 +45,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.ProgramResult; import org.tron.common.runtime.vm.LogInfo; +import org.tron.common.utils.Pair; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; @@ -49,6 +53,7 @@ import org.tron.core.capsule.TransactionInfoCapsule; import org.tron.core.capsule.utils.TransactionUtil; import org.tron.core.config.args.Args; +import org.tron.core.db2.ISession; import org.tron.core.exception.ContractSizeNotEqualToOneException; import org.tron.core.exception.DupTransactionException; import org.tron.core.exception.ItemNotFoundException; @@ -564,4 +569,157 @@ public void testPostContractTriggerSwallowsThrowable() throws Exception { } } + /** + * Covers the fork-replay signature recheck added in this PR: + * when a block being re-applied during switchFork fails witness signature + * validation, the new `if (!validateSignature) throw` block must fire, + * surfacing ValidateSignatureException through the existing catch list. + * + *

Strategy: spy(Manager), inject mocked khaosDb/revokingStore/chainBaseManager + * so switchFork enters the first apply loop with a single mock block whose + * validateSignature returns false. The throw is exercised; downstream + * switchback/finally exceptions from partially-mocked applyBlock are tolerated + * since the throw line is already executed before they run. + */ + @SneakyThrows + @Test + public void testSwitchForkRejectsBlockWithInvalidSignature() { + Manager dbManager = spy(new Manager()); + + // chainBaseManager + stores so getDynamicPropertiesStore() / getAccountStore() resolve. + ChainBaseManager cbm = mock(ChainBaseManager.class); + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + AccountStore accountStore = mock(AccountStore.class); + Sha256Hash sharedHash = Sha256Hash.ZERO_HASH; + when(cbm.getDynamicPropertiesStore()).thenReturn(dps); + when(cbm.getAccountStore()).thenReturn(accountStore); + when(dps.getLatestBlockHeaderHash()).thenReturn(sharedHash); + setField(dbManager, "chainBaseManager", cbm); + + // revokingStore.buildSession() returns a no-op ISession. + RevokingDatabase revokingStore = mock(RevokingDatabase.class); + ISession session = mock(ISession.class); + when(revokingStore.buildSession()).thenReturn(session); + setField(dbManager, "revokingStore", revokingStore); + + // khaosDb.getBranch returns (first=[badBlock], value=[oldBlock]). + // The bad block goes into the apply loop; the old block lets the while + // loops in the rollback/switchback paths exit immediately by matching + // parent hash to the current head hash. + KhaosDatabase khaosDb = mock(KhaosDatabase.class); + setField(dbManager, "khaosDb", khaosDb); + + BlockCapsule badBlock = mock(BlockCapsule.class); + BlockCapsule.BlockId badBlockId = mock(BlockCapsule.BlockId.class); + when(badBlock.getBlockId()).thenReturn(badBlockId); + when(badBlock.getNum()).thenReturn(100L); + when(badBlock.validateSignature(any(DynamicPropertiesStore.class), + any(AccountStore.class))).thenReturn(false); + + BlockCapsule oldBlock = mock(BlockCapsule.class); + BlockCapsule.BlockId oldBlockId = mock(BlockCapsule.BlockId.class); + when(oldBlock.getBlockId()).thenReturn(oldBlockId); + when(oldBlock.getParentHash()).thenReturn(sharedHash); + + LinkedList first = new LinkedList<>(); + first.add(new KhaosDatabase.KhaosBlock(badBlock)); + LinkedList value = new LinkedList<>(); + value.add(new KhaosDatabase.KhaosBlock(oldBlock)); + when(khaosDb.getBranch(any(BlockCapsule.BlockId.class), any(Sha256Hash.class))) + .thenReturn(new Pair<>(first, value)); + + Method switchFork = Manager.class.getDeclaredMethod("switchFork", BlockCapsule.class); + switchFork.setAccessible(true); + + // The throw fires before the finally's switchback runs. Switchback's applyBlock + // may surface another exception due to partial mocks; we tolerate any throwable + // here because the new code's throw has already been executed (line covered). + try { + switchFork.invoke(dbManager, badBlock); + } catch (Throwable ignored) { + // expected: switchback path partially mocked + } + + // The fix's contract: validateSignature was invoked on the replayed block. + verify(badBlock, atLeastOnce()).validateSignature( + any(DynamicPropertiesStore.class), any(AccountStore.class)); + } + + /** + * Symmetric "happy path" coverage: when validateSignature returns true, the + * throw is skipped and execution continues to applyBlock. Pins that the + * new check correctly inverts the boolean (no off-by-one in the `!`). + */ + @SneakyThrows + @Test + public void testSwitchForkPassesValidSignatureBlockToApply() { + Manager dbManager = spy(new Manager()); + + ChainBaseManager cbm = mock(ChainBaseManager.class); + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + AccountStore accountStore = mock(AccountStore.class); + Sha256Hash sharedHash = Sha256Hash.ZERO_HASH; + when(cbm.getDynamicPropertiesStore()).thenReturn(dps); + when(cbm.getAccountStore()).thenReturn(accountStore); + when(dps.getLatestBlockHeaderHash()).thenReturn(sharedHash); + setField(dbManager, "chainBaseManager", cbm); + + RevokingDatabase revokingStore = mock(RevokingDatabase.class); + ISession session = mock(ISession.class); + when(revokingStore.buildSession()).thenReturn(session); + setField(dbManager, "revokingStore", revokingStore); + + KhaosDatabase khaosDb = mock(KhaosDatabase.class); + setField(dbManager, "khaosDb", khaosDb); + + BlockCapsule goodBlock = mock(BlockCapsule.class); + BlockCapsule.BlockId goodBlockId = mock(BlockCapsule.BlockId.class); + when(goodBlock.getBlockId()).thenReturn(goodBlockId); + when(goodBlock.getNum()).thenReturn(100L); + when(goodBlock.validateSignature(any(DynamicPropertiesStore.class), + any(AccountStore.class))).thenReturn(true); + // setSwitch returns self for chained call from applyBlock argument expression. + when(goodBlock.setSwitch(true)).thenReturn(goodBlock); + + LinkedList first = new LinkedList<>(); + first.add(new KhaosDatabase.KhaosBlock(goodBlock)); + LinkedList value = new LinkedList<>(); + when(khaosDb.getBranch(any(BlockCapsule.BlockId.class), any(Sha256Hash.class))) + .thenReturn(new Pair<>(first, value)); + + Method switchFork = Manager.class.getDeclaredMethod("switchFork", BlockCapsule.class); + switchFork.setAccessible(true); + try { + switchFork.invoke(dbManager, goodBlock); + } catch (Throwable ignored) { + // applyBlock against a mocked BlockCapsule will NPE somewhere; tolerated. + } + + // Validation ran AND setSwitch was reached — proves the `if` did not short-circuit + // on the false branch when validateSignature returned true. + verify(goodBlock, atLeastOnce()).validateSignature( + any(DynamicPropertiesStore.class), any(AccountStore.class)); + verify(goodBlock, atLeastOnce()).setSwitch(true); + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field f = target.getClass().getSuperclass() != null + ? findField(target.getClass(), name) + : target.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(target, value); + } + + private static Field findField(Class cls, String name) throws NoSuchFieldException { + Class c = cls; + while (c != null) { + try { + return c.getDeclaredField(name); + } catch (NoSuchFieldException e) { + c = c.getSuperclass(); + } + } + throw new NoSuchFieldException(name); + } + } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/db/ManagerTest.java b/framework/src/test/java/org/tron/core/db/ManagerTest.java index 87b4fcfdc77..958a132fbbf 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -2,9 +2,12 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.tron.common.utils.Commons.adjustAssetBalanceV2; import static org.tron.common.utils.Commons.adjustTotalShieldedPoolValue; @@ -23,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,7 +43,14 @@ import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; import org.tron.common.logsfilter.EventPluginLoader; +import org.tron.common.logsfilter.capsule.BlockFilterCapsule; +import org.tron.common.logsfilter.capsule.BlockLogTriggerCapsule; +import org.tron.common.logsfilter.capsule.FilterTriggerCapsule; +import org.tron.common.logsfilter.capsule.LogsFilterCapsule; +import org.tron.common.logsfilter.capsule.TransactionLogTriggerCapsule; +import org.tron.common.logsfilter.capsule.TriggerCapsule; import org.tron.common.logsfilter.trigger.ContractLogTrigger; +import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Commons; @@ -876,6 +887,47 @@ public void getVerifyTxsTest() { Assert.assertEquals(txs.size(), 1); } + @Test + public void getVerifyTxsSkipsBlockWhenPermissionTxAlreadyConsumed() throws Exception { + // Scenario: a permission-change tx (A) for owner X has been processed and consumed, + // so it is no longer in pendingTransactions but ownerAddressSet still contains X. + // A later transfer tx (B) from X with the old signature enters pending with + // isVerified=true. A malicious SR produces a block containing only B (no A). + // getVerifyTxs must place B into the re-verify list rather than calling + // setVerified(true) just because B matches the pending entry. + TransferContract bContract = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom("f1".getBytes())) + .setAmount(7).build(); + TransactionCapsule bTx = new TransactionCapsule(bContract, ContractType.TransferContract); + String hexOwner = ByteArray.toHexString("f1".getBytes()); + + dbManager.getPendingTransactions().clear(); + dbManager.getPendingTransactions().add(bTx); + + Field field = Manager.class.getDeclaredField("ownerAddressSet"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + Set ownerAddressSet = (Set) field.get(dbManager); + Set backup = new HashSet<>(ownerAddressSet); + ownerAddressSet.clear(); + ownerAddressSet.add(hexOwner); + + try { + List blockTxs = new ArrayList<>(); + blockTxs.add(bTx.getInstance()); + BlockCapsule capsule = new BlockCapsule(0, ByteString.EMPTY, 0, blockTxs); + + List txs = dbManager.getVerifyTxs(capsule); + + Assert.assertEquals(1, txs.size()); + Assert.assertEquals(bTx.getTransactionId(), txs.get(0).getTransactionId()); + } finally { + ownerAddressSet.clear(); + ownerAddressSet.addAll(backup); + dbManager.getPendingTransactions().clear(); + } + } + @Test public void doNotSwitch() throws ValidateSignatureException, ContractValidateException, ContractExeException, @@ -1335,7 +1387,8 @@ public void isExchangeTransactionNonExchangeContractReturnsFalse() throws Except @Test public void blockTrigger() { Manager manager = spy(new Manager()); - doThrow(new RuntimeException("postBlockTrigger mock")).when(manager).postBlockTrigger(any()); + doThrow(new RuntimeException("postBlockTrigger mock")).when(manager) + .postBlockTrigger(any(), anyBoolean()); TronError thrown = Assert.assertThrows(TronError.class, () -> manager.blockTrigger(new BlockCapsule(Block.newBuilder().build()), 1, 1)); Assert.assertEquals(TronError.ErrCode.EVENT_SUBSCRIBE_ERROR, thrown.getErrCode()); @@ -1379,6 +1432,142 @@ public void testReOrgContractTriggerClearsMap() throws Exception { } } + private EventPluginLoader installMockLoader() throws Exception { + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", true); + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, mockLoader); + return mockLoader; + } + + private void restoreLoader(EventPluginLoader original) throws Exception { + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, original); + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", false); + dbManager.getTriggerCapsuleQueue().clear(); + } + + private BlockCapsule blockWithOneTransfer() { + BlockCapsule block = new BlockCapsule(1, chainManager.getGenesisBlockId(), + System.currentTimeMillis(), ByteString.EMPTY); + TransferContract tc = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(new byte[21])) + .setToAddress(ByteString.copyFrom(new byte[21])) + .setAmount(1L).build(); + block.addTransaction(new TransactionCapsule(tc, ContractType.TransferContract)); + return block; + } + + @Test + public void testReOrgBlockTriggerRemoved() throws Exception { + // version-0 reorg emit core: postBlockTrigger threads the removed flag onto both the block + // and transaction triggers. reOrgBlockTrigger calls postBlockTrigger(block, true) (rollback), + // reApplyBlockEvents calls postBlockTrigger(block, false) (forward); both delegate here. + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + EventPluginLoader mockLoader = installMockLoader(); + when(mockLoader.isBlockLogTriggerEnable()).thenReturn(true); + when(mockLoader.isBlockLogTriggerSolidified()).thenReturn(false); + when(mockLoader.isTransactionLogTriggerEnable()).thenReturn(true); + when(mockLoader.isTransactionLogTriggerSolidified()).thenReturn(false); + when(mockLoader.isTransactionLogTriggerEthCompatible()).thenReturn(false); + + BlockingQueue queue = dbManager.getTriggerCapsuleQueue(); + queue.clear(); + BlockCapsule block = blockWithOneTransfer(); + try { + // rollback: block + transaction triggers re-emitted with removed=true + dbManager.postBlockTrigger(block, true); + Assert.assertEquals(2, queue.size()); + Assert.assertTrue(((BlockLogTriggerCapsule) queue.poll()).getBlockLogTrigger().isRemoved()); + Assert.assertTrue(((TransactionLogTriggerCapsule) queue.poll()) + .getTransactionLogTrigger().isRemoved()); + + // forward: removed=false + dbManager.postBlockTrigger(block, false); + Assert.assertEquals(2, queue.size()); + Assert.assertFalse(((BlockLogTriggerCapsule) queue.poll()).getBlockLogTrigger().isRemoved()); + Assert.assertFalse(((TransactionLogTriggerCapsule) queue.poll()) + .getTransactionLogTrigger().isRemoved()); + } finally { + restoreLoader(originalLoader); + } + } + + @Test + public void testReApplyBlockEvents() throws Exception { + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + EventPluginLoader mockLoader = installMockLoader(); + when(mockLoader.getVersion()).thenReturn(0); + when(mockLoader.isBlockLogTriggerEnable()).thenReturn(true); + when(mockLoader.isBlockLogTriggerSolidified()).thenReturn(false); + when(mockLoader.isTransactionLogTriggerEnable()).thenReturn(false); + + BlockingQueue queue = dbManager.getTriggerCapsuleQueue(); + queue.clear(); + BlockCapsule block = new BlockCapsule(1, chainManager.getGenesisBlockId(), + System.currentTimeMillis(), ByteString.EMPTY); + List branch = new ArrayList<>(); + branch.add(new KhaosDatabase.KhaosBlock(block)); + try { + Method m = Manager.class.getDeclaredMethod("reApplyBlockEvents", List.class); + m.setAccessible(true); + m.invoke(dbManager, branch); + // forward block trigger emitted for the re-applied fork branch (removed=false) + Assert.assertEquals(1, queue.size()); + Assert.assertFalse(((BlockLogTriggerCapsule) queue.poll()) + .getBlockLogTrigger().isRemoved()); + } finally { + restoreLoader(originalLoader); + } + } + + @Test + public void testReOrgBlockTrigger() throws Exception { + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + EventPluginLoader mockLoader = installMockLoader(); + when(mockLoader.isBlockLogTriggerEnable()).thenReturn(true); + when(mockLoader.isTransactionLogTriggerEnable()).thenReturn(false); + try { + Method m = Manager.class.getDeclaredMethod("reOrgBlockTrigger"); + m.setAccessible(true); + // exercises the fetch of the old head block + try/catch; must not throw + m.invoke(dbManager); + } finally { + restoreLoader(originalLoader); + } + } + + @Test + public void testPostSolidityTriggerSolidified() throws Exception { + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + EventPluginLoader mockLoader = installMockLoader(); + when(mockLoader.isBlockLogTriggerEnable()).thenReturn(true); + when(mockLoader.isBlockLogTriggerSolidified()).thenReturn(true); + when(mockLoader.isTransactionLogTriggerEnable()).thenReturn(true); + when(mockLoader.isTransactionLogTriggerSolidified()).thenReturn(true); + when(mockLoader.isTransactionLogTriggerEthCompatible()).thenReturn(false); + // make getContinuousBlockCapsule cover the current head block + ReflectUtils.setFieldValue(dbManager, "lastUsedSolidityNum", -1L); + try { + Method m = Manager.class.getDeclaredMethod("postSolidityTrigger", long.class); + m.setAccessible(true); + // exercises the solidified-mode block/transaction batch emission + m.invoke(dbManager, dbManager.getHeadBlockNum()); + } finally { + restoreLoader(originalLoader); + } + } + @Test public void testClearSolidityContractTriggerCache() throws Exception { long blockNum = 999L; @@ -1498,4 +1687,203 @@ public void adjustBalance(AccountStore accountStore, byte[] accountAddress, long Commons.adjustBalance(accountStore, accountAddress, amount, chainManager.getDynamicPropertiesStore().disableJavaLangMath()); } + + /** + * Drives a real reorg and asserts what Manager posts to the jsonrpc filterCapsuleQueue. + */ + @Test + public void switchForkShouldPostFullNodeFilterForNewBranch() throws Exception { + CommonParameter.getInstance().jsonRpcHttpFullNodeEnable = true; + // filterProcessLoop only starts when isJsonRpcFilterEnabled() held at Manager.init() time; it + // was false then, so filterCapsuleQueue is produce-only here and fully observable. + + // bootstrap a head with a known witness + String key = PublicMethod.getRandomPrivateKey(); + byte[] privateKey = ByteArray.fromHexString(key); + final ECKey ecKey = ECKey.fromPrivate(privateKey); + byte[] address = ecKey.getAddress(); + ByteString addressByte = ByteString.copyFrom(address); + chainManager.getAccountStore().put(addressByte.toByteArray(), + new AccountCapsule(Protocol.Account.newBuilder().setAddress(addressByte).build())); + WitnessCapsule witnessCapsule = new WitnessCapsule(addressByte); + chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chainManager.addWitness(addressByte); + chainManager.getWitnessStore().put(address, witnessCapsule); + Block block = blockGenerate.getSignedBlock( + witnessCapsule.getAddress(), 1533529947843L, privateKey); + dbManager.pushBlock(new BlockCapsule(block)); + + Map keys = addTestWitnessAndAccount(); + keys.put(addressByte, key); + + // fund an owner; transfers go owner -> witness 'address' (an existing account) + ECKey ownerKey = new ECKey(Utils.getRandom()); + byte[] owner = ownerKey.getAddress(); + AccountCapsule ownerAccount = new AccountCapsule( + Protocol.Account.newBuilder().setAddress(ByteString.copyFrom(owner)).build()); + ownerAccount.setBalance(1_000_000_000L); + chainManager.getAccountStore().put(owner, ownerAccount); + + long t = 1533529947843L; + long base = chainManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + + // common ancestor P (empty) — fork point and tapos reference + BlockCapsule p = createTestBlockCapsule(t + 3000, base + 1, + chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash().getByteString(), keys); + dbManager.pushBlock(p); + + long expiration = t + 1_000_000L; + BlockingQueue queue = + ReflectUtils.getFieldValue(dbManager, "filterCapsuleQueue"); + queue.clear(); + + // old branch: A carries a transfer; applied via the normal extend path + BlockCapsule a = blockWithTransfer(t + 6000, base + 2, p.getBlockId().getByteString(), keys, + transfer(owner, address, 1L, p, expiration)); + dbManager.pushBlock(a); + Assert.assertEquals("control: head should be A after normal extend", + a.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash()); + Assert.assertTrue("control: normal-path block A's logs must reach FULL stream (added)", + hasLogsFilterCapsule(queue, a, false)); + Assert.assertTrue("control: normal-path block A must reach the FULL block-filter stream", + hasBlockFilterCapsule(queue, a)); + + // heavier competing branch P -> B1 -> B2, each carrying a transfer, to force switchFork + BlockCapsule b1 = blockWithTransfer(t + 6001, base + 2, p.getBlockId().getByteString(), keys, + transfer(owner, address, 2L, p, expiration)); + dbManager.pushBlock(b1); // num <= head -> kept in khaosDb, no switch yet + BlockCapsule b2 = blockWithTransfer(t + 9000, base + 3, b1.getBlockId().getByteString(), keys, + transfer(owner, address, 3L, p, expiration)); + dbManager.pushBlock(b2); // num > head & parent != head -> triggers switchFork + + Assert.assertEquals("reorg must switch the canonical head to the competing branch (B2)", + b2.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash()); + + // reorg withdraws the orphaned old-branch logs (removed=true) + Assert.assertTrue("reorg: orphaned block A's logs must be withdrawn (removed=true)", + hasLogsFilterCapsule(queue, a, true)); + // the fix: new canonical blocks' logs and block filters are delivered + Assert.assertTrue("reorg: new canonical block B1's logs must reach FULL stream (added)", + hasLogsFilterCapsule(queue, b1, false)); + Assert.assertTrue("reorg: new canonical block B2's logs must reach FULL stream (added)", + hasLogsFilterCapsule(queue, b2, false)); + Assert.assertTrue("reorg: new canonical block B1 must reach the FULL block-filter stream", + hasBlockFilterCapsule(queue, b1)); + Assert.assertTrue("reorg: new canonical block B2 must reach the FULL block-filter stream", + hasBlockFilterCapsule(queue, b2)); + } + + /** + * A fork switch re-applies the new branch on a rewound, diverged state, so any signature + * verification cached on those transactions (isVerified) must be cleared to force + * re-validation against the fork-chain state. Drives a real reorg and asserts that switchFork + * resets isVerified on the transactions of the branch it switches to. + */ + @Test + public void switchForkShouldResetTransactionSignVerifiedOnNewBranch() throws Exception { + // bootstrap a head with a known witness + String key = PublicMethod.getRandomPrivateKey(); + byte[] privateKey = ByteArray.fromHexString(key); + final ECKey ecKey = ECKey.fromPrivate(privateKey); + byte[] address = ecKey.getAddress(); + ByteString addressByte = ByteString.copyFrom(address); + chainManager.getAccountStore().put(addressByte.toByteArray(), + new AccountCapsule(Protocol.Account.newBuilder().setAddress(addressByte).build())); + WitnessCapsule witnessCapsule = new WitnessCapsule(addressByte); + chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chainManager.addWitness(addressByte); + chainManager.getWitnessStore().put(address, witnessCapsule); + Block block = blockGenerate.getSignedBlock( + witnessCapsule.getAddress(), 1533529947843L, privateKey); + dbManager.pushBlock(new BlockCapsule(block)); + + Map keys = addTestWitnessAndAccount(); + keys.put(addressByte, key); + + // fund an owner; transfers go owner -> witness 'address' (an existing account) + ECKey ownerKey = new ECKey(Utils.getRandom()); + byte[] owner = ownerKey.getAddress(); + AccountCapsule ownerAccount = new AccountCapsule( + Protocol.Account.newBuilder().setAddress(ByteString.copyFrom(owner)).build()); + ownerAccount.setBalance(1_000_000_000L); + chainManager.getAccountStore().put(owner, ownerAccount); + + long t = 1533529947843L; + long base = chainManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + long expiration = t + 1_000_000L; + + // common ancestor P (empty) — fork point and tapos reference + BlockCapsule p = createTestBlockCapsule(t + 3000, base + 1, + chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash().getByteString(), keys); + dbManager.pushBlock(p); + + // old branch: A extends P via the normal path and becomes head + BlockCapsule a = blockWithTransfer(t + 6000, base + 2, p.getBlockId().getByteString(), keys, + transfer(owner, address, 1L, p, expiration)); + dbManager.pushBlock(a); + Assert.assertEquals("control: head should be A after normal extend", + a.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash()); + + // heavier competing branch P -> B1 -> B2 forces switchFork; spy the tx on the branch we + // switch to and pre-mark it verified to mimic a stale cache computed on a different state + BlockCapsule b1 = blockWithTransfer(t + 6001, base + 2, p.getBlockId().getByteString(), keys, + transfer(owner, address, 2L, p, expiration)); + dbManager.pushBlock(b1); // num <= head -> kept in khaosDb, no switch yet + + TransactionCapsule forkTx = transfer(owner, address, 3L, p, expiration); + forkTx.setVerified(true); + TransactionCapsule spyTx = spy(forkTx); + BlockCapsule b2 = blockWithTransfer(t + 9000, base + 3, b1.getBlockId().getByteString(), keys, + spyTx); + dbManager.pushBlock(b2); // num > head & parent != head -> triggers switchFork + + Assert.assertEquals("reorg must switch the canonical head to the competing branch (B2)", + b2.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash()); + // switchFork must clear the cached verification flag on the new branch's transaction so it + // re-validates against the fork-chain state + verify(spyTx, atLeastOnce()).setVerified(false); + } + + private TransactionCapsule transfer(byte[] owner, byte[] to, long amount, + BlockCapsule refBlock, long expiration) { + TransferContract contract = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(owner)) + .setToAddress(ByteString.copyFrom(to)) + .setAmount(amount).build(); + TransactionCapsule tx = new TransactionCapsule(contract, ContractType.TransferContract); + tx.setReference(refBlock.getNum(), refBlock.getBlockId().getBytes()); + tx.setExpiration(expiration); + return tx; + } + + private BlockCapsule blockWithTransfer(long time, long number, ByteString parentHash, + Map keys, TransactionCapsule tx) { + ByteString witnessAddress = dposSlot.getScheduledWitness(dposSlot.getSlot(time)); + BlockCapsule blockCapsule = new BlockCapsule(number, Sha256Hash.wrap(parentHash), time, + witnessAddress); + blockCapsule.addTransaction(tx); + blockCapsule.generatedByMyself = true; + blockCapsule.setMerkleRoot(); + blockCapsule.sign(ByteArray.fromHexString(keys.get(witnessAddress))); + return blockCapsule; + } + + private boolean hasLogsFilterCapsule(BlockingQueue queue, BlockCapsule b, + boolean removed) { + String blockHash = b.getBlockId().toString(); + return queue.stream() + .filter(c -> c instanceof LogsFilterCapsule) + .map(c -> (LogsFilterCapsule) c) + .anyMatch(c -> !c.isSolidified() && c.isRemoved() == removed + && blockHash.equals(c.getBlockHash())); + } + + private boolean hasBlockFilterCapsule(BlockingQueue queue, + BlockCapsule b) { + String blockHash = b.getBlockId().toString(); + return queue.stream() + .filter(c -> c instanceof BlockFilterCapsule) + .map(c -> (BlockFilterCapsule) c) + .anyMatch(c -> !c.isSolidified() && blockHash.equals(c.getBlockHash())); + } } diff --git a/framework/src/test/java/org/tron/core/db/TransactionHistoryTest.java b/framework/src/test/java/org/tron/core/db/TransactionHistoryTest.java index 676293efbc0..e6d5fbb7bcf 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionHistoryTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionHistoryTest.java @@ -17,7 +17,6 @@ public class TransactionHistoryTest extends BaseTest { private static final byte[] transactionId = TransactionStoreTest.randomBytes(32); private static String dbDirectory = "db_TransactionHistoryStore_test"; - private static String indexDirectory = "index_TransactionHistoryStore_test"; @Resource private TransactionHistoryStore transactionHistoryStore; @@ -27,8 +26,7 @@ public class TransactionHistoryTest extends BaseTest { Args.setParam( new String[]{ "--output-directory", dbPath(), - "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory + "--storage-db-directory", dbDirectory }, TestConstants.TEST_CONF ); diff --git a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java index 6cd7af96577..3a13c7d5606 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionRetStoreTest.java @@ -21,7 +21,6 @@ public class TransactionRetStoreTest extends BaseTest { private static final byte[] transactionId = TransactionStoreTest.randomBytes(32); private static final byte[] blockNum = ByteArray.fromLong(1); private static String dbDirectory = "db_TransactionRetStore_test"; - private static String indexDirectory = "index_TransactionRetStore_test"; @Resource private TransactionRetStore transactionRetStore; private static Transaction transaction; @@ -33,8 +32,7 @@ public class TransactionRetStoreTest extends BaseTest { static { Args.setParam(new String[]{"--output-directory", dbPath(), - "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory}, TestConstants.TEST_CONF); + "--storage-db-directory", dbDirectory}, TestConstants.TEST_CONF); } @BeforeClass diff --git a/framework/src/test/java/org/tron/core/db/TransactionStoreTest.java b/framework/src/test/java/org/tron/core/db/TransactionStoreTest.java index 5341cffd171..b79c4cdfc14 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionStoreTest.java @@ -41,7 +41,6 @@ public class TransactionStoreTest extends BaseTest { private static final String WITNESS_ADDRESS = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; private static String dbDirectory = "db_TransactionStore_test"; - private static String indexDirectory = "index_TransactionStore_test"; @Resource private TransactionStore transactionStore; diff --git a/framework/src/test/java/org/tron/core/db/TransactionTraceTest.java b/framework/src/test/java/org/tron/core/db/TransactionTraceTest.java index 08848fc9da1..5917cd06603 100644 --- a/framework/src/test/java/org/tron/core/db/TransactionTraceTest.java +++ b/framework/src/test/java/org/tron/core/db/TransactionTraceTest.java @@ -22,6 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.ByteArray; @@ -52,7 +53,6 @@ public class TransactionTraceTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static String dbDirectory = "db_TransactionTrace_test"; - private static String indexDirectory = "index_TransactionTrace_test"; private static ByteString ownerAddress = ByteString.copyFrom(ByteArray.fromInt(1)); private static ByteString contractAddress = ByteString.copyFrom(ByteArray.fromInt(2)); private static String OwnerAddress = "TCWHANtDDdkZCTo2T2peyEq3Eg9c2XB7ut"; @@ -64,10 +64,9 @@ public class TransactionTraceTest extends BaseTest { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } diff --git a/framework/src/test/java/org/tron/core/db/TxCacheDBTest.java b/framework/src/test/java/org/tron/core/db/TxCacheDBTest.java index e47ef72a29d..1793fb0e6a9 100644 --- a/framework/src/test/java/org/tron/core/db/TxCacheDBTest.java +++ b/framework/src/test/java/org/tron/core/db/TxCacheDBTest.java @@ -18,9 +18,8 @@ public class TxCacheDBTest extends BaseTest { @BeforeClass public static void init() { String dbDirectory = "db_TransactionCache_test"; - String indexDirectory = "index_TransactionCache_test"; Args.setParam(new String[]{"--output-directory", dbPath(), "--storage-db-directory", - dbDirectory, "--storage-index-directory", indexDirectory}, TestConstants.TEST_CONF); + dbDirectory}, TestConstants.TEST_CONF); } @Test diff --git a/framework/src/test/java/org/tron/core/db/api/AssetUpdateHelperTest.java b/framework/src/test/java/org/tron/core/db/api/AssetUpdateHelperTest.java index d1edd92c109..be4a0b87c1a 100644 --- a/framework/src/test/java/org/tron/core/db/api/AssetUpdateHelperTest.java +++ b/framework/src/test/java/org/tron/core/db/api/AssetUpdateHelperTest.java @@ -8,6 +8,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.AccountCapsule; @@ -27,7 +28,7 @@ public class AssetUpdateHelperTest extends BaseTest { private static boolean init; static { - Args.setParam(new String[]{"-d", dbPath()}, "config-test-index.conf"); + Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); Args.getInstance().setSolidityNode(true); } diff --git a/framework/src/test/java/org/tron/core/event/RealtimeEventServiceTest.java b/framework/src/test/java/org/tron/core/event/RealtimeEventServiceTest.java index 91dcea71322..f58f725195c 100644 --- a/framework/src/test/java/org/tron/core/event/RealtimeEventServiceTest.java +++ b/framework/src/test/java/org/tron/core/event/RealtimeEventServiceTest.java @@ -5,21 +5,17 @@ import com.google.protobuf.ByteString; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.BlockingQueue; -import org.eclipse.jetty.util.BlockingArrayQueue; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import org.tron.common.logsfilter.EventPluginLoader; import org.tron.common.logsfilter.capsule.BlockLogTriggerCapsule; import org.tron.common.logsfilter.capsule.TransactionLogTriggerCapsule; -import org.tron.common.logsfilter.capsule.TriggerCapsule; import org.tron.common.logsfilter.trigger.ContractEventTrigger; import org.tron.common.logsfilter.trigger.ContractLogTrigger; import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; -import org.tron.core.db.Manager; import org.tron.core.services.event.BlockEventCache; import org.tron.core.services.event.RealtimeEventService; import org.tron.core.services.event.bo.BlockEvent; @@ -56,39 +52,39 @@ public void test() throws Exception { EventPluginLoader instance = mock(EventPluginLoader.class); ReflectUtils.setFieldValue(realtimeEventService, "instance", instance); - BlockingQueue queue = new BlockingArrayQueue<>(); - Manager manager = mock(Manager.class); - Mockito.when(manager.getTriggerCapsuleQueue()).thenReturn(queue); - ReflectUtils.setFieldValue(realtimeEventService, "manager", manager); - BlockCapsule blockCapsule = new BlockCapsule(0L, Sha256Hash.ZERO_HASH, 0L, ByteString.copyFrom(BlockEventCacheTest.getBlockId())); - be2.setBlockLogTriggerCapsule(new BlockLogTriggerCapsule(blockCapsule)); + // spy so processTrigger() is a no-op (does not reach the real EventPluginLoader), + // while setRemoved() still mutates the real trigger so the removed flag can be asserted. + BlockLogTriggerCapsule blockCap = Mockito.spy(new BlockLogTriggerCapsule(blockCapsule)); + Mockito.doNothing().when(blockCap).processTrigger(); + be2.setBlockLogTriggerCapsule(blockCap); Mockito.when(instance.isBlockLogTriggerEnable()).thenReturn(true); Mockito.when(instance.isBlockLogTriggerSolidified()).thenReturn(false); - realtimeEventService.add(event); - realtimeEventService.work(); - - Assert.assertEquals(0, queue.size()); - - event = new Event(be2, false); - realtimeEventService.add(event); - realtimeEventService.work(); + // reorg rollback: block trigger re-emitted (posted synchronously) with removed=true + realtimeEventService.flush(be2, true); + Assert.assertTrue(blockCap.getBlockLogTrigger().isRemoved()); - Assert.assertEquals(1, queue.size()); + // forward: block trigger posted with removed=false + realtimeEventService.flush(be2, false); + Assert.assertFalse(blockCap.getBlockLogTrigger().isRemoved()); + // posted directly to the plugin both times, never via the async queue + Mockito.verify(blockCap, Mockito.times(2)).processTrigger(); be2.setBlockLogTriggerCapsule(null); - queue.poll(); + TransactionLogTriggerCapsule txCap = mock(TransactionLogTriggerCapsule.class); List list = new ArrayList<>(); - list.add(mock(TransactionLogTriggerCapsule.class)); + list.add(txCap); be2.setTransactionLogTriggerCapsules(list); - Mockito.when(instance.isTransactionLogTriggerEnable()).thenReturn(true); Mockito.when(instance.isTransactionLogTriggerSolidified()).thenReturn(false); - realtimeEventService.flush(be2, event.isRemove()); - Assert.assertEquals(1, queue.size()); + + // rollback: tx trigger posted synchronously with removed=true + realtimeEventService.flush(be2, true); + Mockito.verify(txCap).setRemoved(true); + Mockito.verify(txCap).processTrigger(); be2.setTransactionLogTriggerCapsules(null); diff --git a/framework/src/test/java/org/tron/core/jsonrpc/ApiUtilTest.java b/framework/src/test/java/org/tron/core/jsonrpc/ApiUtilTest.java index f62d47d5367..a74ca3a69a4 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/ApiUtilTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/ApiUtilTest.java @@ -8,6 +8,7 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; @@ -21,7 +22,7 @@ public class ApiUtilTest { @BeforeClass public static void init() { - Args.setParam(new String[]{}, "config-localtest.conf"); + Args.setParam(new String[]{}, TestConstants.TEST_CONF); } @AfterClass diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java index 5f577194dff..49f875f3823 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcTest.java @@ -143,13 +143,13 @@ public void testAddressCompatibleToByteArray() { try { addressCompatibleToByteArray(rawAddress.substring(1)); } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("invalid address hash value", e.getMessage()); + Assert.assertEquals("invalid address", e.getMessage()); } try { addressCompatibleToByteArray(rawAddress + "00"); } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("invalid address hash value", e.getMessage()); + Assert.assertEquals("invalid address", e.getMessage()); } } @@ -177,6 +177,22 @@ public void testAddressToByteArray() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } + + // oversized input rejected before fromHexString + try { + addressToByteArray("0x" + new String(new char[64]).replace('\0', 'a')); + Assert.fail(); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("invalid address")); + } + + // invalid hex char -> invalid params, not a leaked DecoderException + try { + addressToByteArray("0x548794500882809695a8a687866e76d4271a1abz"); + Assert.fail(); + } catch (JsonRpcInvalidParamsException e) { + Assert.assertTrue(e.getMessage().contains("invalid address")); + } } /** @@ -185,7 +201,7 @@ public void testAddressToByteArray() { @Test public void testLogFilter() { - //topic must be 64 hex string + //topic must be 63 or 64 hex string, full 64-char form here try { new LogFilter(new FilterRequest(null, null, null, new String[] {"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"}, @@ -194,6 +210,19 @@ public void testLogFilter() { Assert.fail(); } + //63-char form: leading zero stripped by some clients, padded back to the same topic + String paddedAddressTopic = + "000000000000000000000000f0cc5a2a84cd0f68ed1667070934542d673acbd8"; + try { + LogFilter full = new LogFilter(new FilterRequest(null, null, null, + new String[] {null, "0x" + paddedAddressTopic}, null)); + LogFilter stripped = new LogFilter(new FilterRequest(null, null, null, + new String[] {null, "0x" + paddedAddressTopic.substring(1)}, null)); + Assert.assertArrayEquals(full.getTopics().get(1)[0], stripped.getTopics().get(1)[0]); + } catch (JsonRpcInvalidParamsException e) { + Assert.fail(); + } + try { new LogFilter(new FilterRequest(null, null, null, new String[] {"0x0"}, null)); } catch (JsonRpcInvalidParamsException e) { @@ -209,6 +238,20 @@ public void testLogFilter() { Assert.assertTrue(e.getMessage().contains("invalid topic")); } + // non-string element in address array -> -32602, not a leaked ClassCastException + JsonRpcInvalidParamsException badAddrElement = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilter(new FilterRequest(null, null, + new ArrayList<>(Collections.singletonList(1)), null, null))); + Assert.assertEquals("invalid address at index 0: 1", badAddrElement.getMessage()); + + // non-string element in nested topic array -> -32602, not a leaked ClassCastException + JsonRpcInvalidParamsException badTopicElement = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilter(new FilterRequest(null, null, null, + new Object[] {new ArrayList<>(Collections.singletonList(1))}, null))); + Assert.assertEquals("invalid topic(s): 1", badTopicElement.getMessage()); + // topic size should be <= 4 try { new LogFilter(new FilterRequest(null, null, null, diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index f753045d259..e8d14ace060 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -26,6 +26,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.bouncycastle.util.encoders.Hex; +import org.eclipse.jetty.http.HttpStatus; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -56,6 +57,7 @@ import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; +import org.tron.core.services.jsonrpc.types.BuildArguments; import org.tron.core.services.jsonrpc.types.TransactionReceipt; import org.tron.core.services.jsonrpc.types.TransactionResult; import org.tron.json.JSON; @@ -514,11 +516,9 @@ public void testBlockTagParsing() { () -> parseBlockNumber("abc", wallet)); Assert.assertEquals("Incorrect hex syntax", abcEx.getMessage()); - // parseBlockNumber: malformed hex -> throws Exception hexEx = Assert.assertThrows(Exception.class, () -> parseBlockNumber("0xxabc", wallet)); - // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed - Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); + Assert.assertEquals("invalid block number", hexEx.getMessage()); } @Test @@ -579,10 +579,29 @@ public void testGetStorageAt() { () -> tronJsonRpc.getStorageAt("", "", "abc")); Assert.assertEquals("invalid block number", e6.getMessage()); + // storageIdx length oversized -> invalid storage key value + String addr = "0xabd4b9367799eaa3197fecb144eb71de1e049abc"; + Exception e7 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, + "0x" + new String(new char[65]).replace('\0', 'a'), "latest")); + Assert.assertEquals("invalid storage key value", e7.getMessage()); + + // storageIdx is null -> invalid storage key value + Exception e8 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, null, "latest")); + Assert.assertEquals("invalid storage key value", e8.getMessage()); + + // storageIdx is valid length but decodes to >32 bytes (66 hex chars without 0x = 33 bytes): + // DataWord rejects it -> invalid storage key value (-32602), not a leaked Internal error. + Exception e9 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt(addr, + new String(new char[66]).replace('\0', 'a'), "latest")); + Assert.assertEquals("invalid storage key value", e9.getMessage()); + // latest happy path: address is an account, not a contract, so returns 32 zero bytes try { String value = tronJsonRpc.getStorageAt( - "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "0x0", "latest"); + addr, "0x0", "latest"); Assert.assertEquals(ByteArray.toJsonHex(new byte[32]), value); } catch (Exception e) { Assert.fail(); @@ -674,6 +693,31 @@ public void testGetTransactionByBlockNumberAndIndex() { Assert.fail(); } + // negative index is out of range too -> null (not an Internal error) + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x-1"); + Assert.assertNull(result); + } catch (Exception e) { + Assert.fail(); + } + + // leading zeros are tolerated: "0x00" parses to index 0 + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x00"); + Assert.assertNotNull(result); + Assert.assertEquals(ByteArray.toJsonHex(0L), result.getTransactionIndex()); + } catch (Exception e) { + Assert.fail(); + } + + // oversized index (> 8 hex digits) rejected before parsing + Exception oversizedEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x123456789")); + Assert.assertEquals("invalid index value", oversizedEx.getMessage()); + // latest -> blockCapsule1 (head) try { TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("latest", "0x0"); @@ -1006,6 +1050,29 @@ public void testLogFilterWrapper() { Assert.fail(); } + JsonRpcInvalidParamsException shortHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, "0x111111"), + 100, null, false)); + Assert.assertEquals("invalid hash value", shortHashEx.getMessage()); + + JsonRpcInvalidParamsException oversizedHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, + "0x" + new String(new char[1000]).replace('\0', 'a')), + 100, null, false)); + Assert.assertEquals("invalid hash value", oversizedHashEx.getMessage()); + + JsonRpcInvalidParamsException validHashEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest(null, null, null, + null, + "0x" + new String(new char[64]).replace('\0', 'a')), + 100, null, false)); + Assert.assertEquals("invalid blockHash", validHashEx.getMessage()); + // reset Args.getInstance().setJsonRpcMaxBlockRange(oldMaxBlockRange); } @@ -1262,6 +1329,35 @@ public void testLogFilterTagHandling() { } } + @Test + public void testGetFilterLogsEnforcesBlockRange() throws Exception { + int oldMaxBlockRange = Args.getInstance().getJsonRpcMaxBlockRange(); + Args.getInstance().setJsonRpcMaxBlockRange(5_000); + try { + // toBlock=0xf4240 (1_000_000) is clamped to head (LATEST_BLOCK_NUM = 10_000), so the span + // is min(1_000_000, 10_000) - 0 = 10_000 > 5_000 cap. + String wideFilterId = tronJsonRpc.newFilter( + new FilterRequest("0x0", "0xf4240", null, null, null)); + Assert.assertNotNull(wideFilterId); + + // eth_getFilterLogs must reject it at query time (previously bypassed -> unbounded scan). + JsonRpcInvalidParamsException ex = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.getFilterLogs(wideFilterId)); + Assert.assertEquals("exceed max block range: 5000", ex.getMessage()); + + // A within-cap filter (span 0) is still served normally - no false rejection. + String headHex = ByteArray.toJsonHex(blockCapsule1.getNum()); + int expectedLogs = blockCapsule1.getTransactions().size() * 2; + String okFilterId = tronJsonRpc.newFilter( + new FilterRequest(headHex, headHex, null, null, null)); + LogFilterElement[] okResult = tronJsonRpc.getFilterLogs(okFilterId); + Assert.assertEquals(expectedLogs, okResult.length); + } finally { + Args.getInstance().setJsonRpcMaxBlockRange(oldMaxBlockRange); + } + } + @Test public void testGetBlockReceipts() { @@ -1312,6 +1408,10 @@ public void testGetBlockReceipts() { () -> tronJsonRpc.getBlockReceipts("test")); Assert.assertEquals("invalid block number", testReceiptsEx.getMessage()); + Exception nullReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts(null)); + Assert.assertEquals("invalid block number", nullReceiptsEx.getMessage()); + try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); Assert.assertNull(transactionReceiptList); @@ -1389,6 +1489,75 @@ public void testBuildTransactionTransfer() { } } + @Test + public void testBuildCreateSmartContractAcceptsNullAbiOutputsOverHttp() { + fullNodeJsonRpcHttpService.start(); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + JsonObject buildArgs = new JsonObject(); + buildArgs.addProperty("from", "0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + buildArgs.addProperty("data", "608060405234801561001057600080fd5b50"); + buildArgs.addProperty("gas", "0x3b9aca00"); + buildArgs.addProperty("abi", "[{\"inputs\":[],\"name\":\"test\",\"outputs\":null," + + "\"type\":\"function\"}]"); + JsonArray params = new JsonArray(); + params.add(buildArgs); + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("jsonrpc", "2.0"); + requestBody.addProperty("method", "buildTransaction"); + requestBody.add("params", params); + requestBody.addProperty("id", 1); + + HttpPost httpPost = new HttpPost("http://127.0.0.1:" + + CommonParameter.getInstance().getJsonRpcHttpFullNodePort() + "/jsonrpc"); + httpPost.addHeader("Content-Type", "application/json"); + httpPost.setEntity(new StringEntity(requestBody.toString())); + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + String resp = EntityUtils.toString(response.getEntity()); + JSONObject json = JSON.parseObject(resp); + Assert.assertNull(resp, json.getJSONObject("error")); + JSONObject tx = json.getJSONObject("result").getJSONObject("transaction"); + Assert.assertNotNull("transaction must be a JSON object", tx); + + JSONArray contracts = tx.getJSONObject("raw_data").getJSONArray("contract"); + Assert.assertEquals(1, contracts.size()); + JSONObject contract = contracts.getJSONObject(0); + Assert.assertEquals("CreateSmartContract", contract.getString("type")); + JSONObject value = contract.getJSONObject("parameter").getJSONObject("value"); + JSONObject abi = value.getJSONObject("new_contract").getJSONObject("abi"); + JSONArray entrys = abi.getJSONArray("entrys"); + Assert.assertEquals(1, entrys.size()); + Assert.assertFalse(entrys.getJSONObject(0).containsKey("outputs")); + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } finally { + fullNodeJsonRpcHttpService.stop(); + } + } + + @Test + public void testBuildTransactionRejectsDeeplyNestedAbi() { + // A deeply nested ABI must surface as invalid-params (-32602), not as a generic + // internal error. + int depth = 200_000; + StringBuilder abi = new StringBuilder("[],\"x\":"); + for (int i = 0; i < depth; i++) { + abi.append('['); + } + for (int i = 0; i < depth; i++) { + abi.append(']'); + } + + BuildArguments args = new BuildArguments(); + args.setFrom("0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.setData("60806040"); + args.setAbi(abi.toString()); + + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.buildTransaction(args)); + Assert.assertEquals("invalid abi", e.getMessage()); + } + @Test public void testWeb3ClientVersion() { try { @@ -1404,8 +1573,7 @@ public void testWeb3ClientVersion() { * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack. * * Covers: normal request no regression, Content-Length oversized 413, - * and chunked oversized handled gracefully (body truncated, 200 + empty body - * because jsonrpc4j absorbs the BadMessageException). + * and chunked oversized 413 during streaming body reads. */ @Test public void testJsonRpcSizeLimitIntegration() { @@ -1441,11 +1609,11 @@ public void testJsonRpcSizeLimitIntegration() { overPost.setEntity(new StringEntity( new String(new char[(int) testLimit + 1]).replace('\0', 'x'))); resp = httpClient.execute(overPost); - Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, + resp.getStatusLine().getStatusCode()); resp.close(); - // Chunked oversized -> BadMessageException thrown during body read, - // absorbed by jsonrpc4j catch(Exception) -> 200 with empty body. + // Chunked oversized -> BadMessageException thrown during body read. // Body read IS truncated at the limit - OOM protection effective. byte[] chunkedData = new String(new char[(int) testLimit * 2]) .replace('\0', 'x').getBytes("UTF-8"); @@ -1453,10 +1621,8 @@ public void testJsonRpcSizeLimitIntegration() { chunkedPost.setEntity(new InputStreamEntity( new ByteArrayInputStream(chunkedData), -1)); resp = httpClient.execute(chunkedPost); - Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); - body = EntityUtils.toString(resp.getEntity()); - Assert.assertTrue("Chunked oversized should return empty body" - + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); + Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, + resp.getStatusLine().getStatusCode()); resp.close(); } } catch (Exception e) { diff --git a/framework/src/test/java/org/tron/core/metrics/MetricsApiServiceTest.java b/framework/src/test/java/org/tron/core/metrics/MetricsApiServiceTest.java index 6894d91cdbe..f96a03d92e3 100644 --- a/framework/src/test/java/org/tron/core/metrics/MetricsApiServiceTest.java +++ b/framework/src/test/java/org/tron/core/metrics/MetricsApiServiceTest.java @@ -14,7 +14,6 @@ public class MetricsApiServiceTest extends BaseMethodTest { private static String dbDirectory = "metrics-database"; - private static String indexDirectory = "metrics-index"; private static int port = 10001; private MetricsApiService metricsApiService; private RpcApiService rpcApiService; @@ -23,7 +22,6 @@ public class MetricsApiServiceTest extends BaseMethodTest { protected String[] extraArgs() { return new String[]{ "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }; } diff --git a/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java index 2e79bbf5809..52cdfa9c826 100644 --- a/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java +++ b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java @@ -1,8 +1,11 @@ package org.tron.core.net; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import com.google.protobuf.ByteString; import java.lang.reflect.Method; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import org.junit.Assert; @@ -14,6 +17,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; import org.tron.core.config.args.Args; +import org.tron.core.exception.P2pException; import org.tron.core.net.message.TronMessage; import org.tron.core.net.message.adv.FetchInvDataMessage; import org.tron.core.net.message.adv.InventoryMessage; @@ -209,6 +213,35 @@ public void testCheckInvRateLimitBlockBoundary() throws Exception { peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); } + @Test + public void testCheckInvRateLimitUnknownTypeRejected() throws Exception { + CommonParameter parameter = CommonParameter.getInstance(); + parameter.setMaxTps(10); + parameter.setMaxBlockInvPerSecond(10); + + PeerStatistics peerStatistics = new PeerStatistics(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getPeerStatistics()).thenReturn(peerStatistics); + + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + Method method = handler.getClass() + .getDeclaredMethod("processMessage", PeerConnection.class, byte[].class); + method.setAccessible(true); + + // craft an Inventory whose enum type holds an undefined value (2) + Protocol.Inventory inv = Protocol.Inventory.newBuilder() + .setTypeValue(2) + .addIds(ByteString.copyFrom(new byte[32])) + .build(); + InventoryMessage msg = new InventoryMessage(inv.toByteArray()); + Assert.assertEquals(InventoryType.UNRECOGNIZED, msg.getInventoryType()); + + method.invoke(handler, peer, msg.getSendBytes()); + + // checkInvRateLimit throws BAD_MESSAGE -> processException -> disconnect(BAD_PROTOCOL) + verify(peer).disconnect(Protocol.ReasonCode.BAD_PROTOCOL); + } + @Test public void testUpdateLastInteractiveTime() throws Exception { PeerConnection peer = new PeerConnection(); @@ -223,4 +256,53 @@ public void testUpdateLastInteractiveTime() throws Exception { method.invoke(p2pEventHandler, peer, message); Assert.assertTrue(peer.getLastInteractiveTime() >= t1); } + + /** + * Regression for PR #6716: validateMerkleRoot introduced + * P2pException.TypeEnum.BLOCK_MERKLE_INVALID, but processException's switch + * did not include the new type, so the peer was disconnected with + * ReasonCode.UNKNOWN instead of BAD_BLOCK. This test pins that + * BLOCK_MERKLE_INVALID is mapped to BAD_BLOCK (and gets the bad-peer ban + * window via PeerConnection.processDisconnect). + */ + @Test + public void testProcessExceptionMapsBlockMerkleErrorToBadBlock() throws Exception { + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getInetSocketAddress()) + .thenReturn(new InetSocketAddress("127.0.0.1", 18888)); + + P2pException ex = new P2pException( + P2pException.TypeEnum.BLOCK_MERKLE_INVALID, "merkle mismatch"); + + Method method = handler.getClass().getDeclaredMethod("processException", + PeerConnection.class, TronMessage.class, Exception.class); + method.setAccessible(true); + method.invoke(handler, peer, null, ex); + + verify(peer).disconnect(Protocol.ReasonCode.BAD_BLOCK); + } + + /** + * Companion sanity check: BLOCK_SIGN_INVALID already mapped correctly + * before this fix; pin it so future refactors do not silently drop it + * (or BLOCK_MERKLE_INVALID) back to UNKNOWN. + */ + @Test + public void testProcessExceptionMapsBlockSignErrorToBadBlock() throws Exception { + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getInetSocketAddress()) + .thenReturn(new InetSocketAddress("127.0.0.1", 18888)); + + P2pException ex = new P2pException( + P2pException.TypeEnum.BLOCK_SIGN_INVALID, "bad signature"); + + Method method = handler.getClass().getDeclaredMethod("processException", + PeerConnection.class, TronMessage.class, Exception.class); + method.setAccessible(true); + method.invoke(handler, peer, null, ex); + + verify(peer).disconnect(Protocol.ReasonCode.BAD_BLOCK); + } } diff --git a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java index 7e584581d2b..4c16f28930c 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -169,7 +169,7 @@ public void testValidBlockMerkleRoot() throws Exception { tronNetDelegate.validBlock(tampered); Assert.fail("Expected P2pException for tampered merkle root"); } catch (P2pException e) { - Assert.assertEquals(TypeEnum.BLOCK_MERKLE_ERROR, e.getType()); + Assert.assertEquals(TypeEnum.BLOCK_MERKLE_INVALID, e.getType()); } } } diff --git a/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java new file mode 100644 index 00000000000..7d883b7207d --- /dev/null +++ b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java @@ -0,0 +1,200 @@ +package org.tron.core.net.message.adv; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import com.google.protobuf.UnknownFieldSet; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.common.overlay.message.Message; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; + +/** + * Verifies the {@code sanitize()} helpers on {@link BlockCapsule}, + * {@link TransactionCapsule} and {@link BlockMessage}: they strip outer + * unknown protobuf fields while leaving every consensus-hashed / signed + * region byte-identical. + */ +public class SanitizeUnknownFieldsTest { + + private static final UnknownFieldSet PADDING = UnknownFieldSet.newBuilder() + .addField(99999, UnknownFieldSet.Field.newBuilder() + .addLengthDelimited(ByteString.copyFrom(new byte[1024])) + .build()) + .build(); + + @BeforeClass + public static void setUp() { + // BlockMessage(byte[]) calls Message.isFilter() which dereferences the + // static DynamicPropertiesStore. The mock's primitive-long getter returns + // 0L by default, so isFilter() returns false. + Message.setDynamicPropertiesStore(Mockito.mock(DynamicPropertiesStore.class)); + } + + private static BlockHeader.raw sampleRawHeader() { + return BlockHeader.raw.newBuilder() + .setNumber(100) + .setTimestamp(123456789L) + .build(); + } + + private static Block sampleBlock() { + return Block.newBuilder() + .setBlockHeader(BlockHeader.newBuilder().setRawData(sampleRawHeader()).build()) + .build(); + } + + private static Transaction sampleTransaction() { + return Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(123456789L).build()) + .build(); + } + + // ---- BlockCapsule.sanitize ---- + + @Test + public void blockCapsuleSanitizeStripsBlockLevelUnknownFields() { + Block padded = sampleBlock().toBuilder().setUnknownFields(PADDING).build(); + BlockCapsule capsule = new BlockCapsule(padded); + long originalSize = capsule.getData().length; + + assertTrue("sanitize() should report it mutated the capsule", capsule.sanitize()); + + assertTrue("Block-level unknown fields should be stripped", + capsule.getInstance().getUnknownFields().asMap().isEmpty()); + assertTrue("Sanitized capsule bytes should shrink", + capsule.getData().length < originalSize); + } + + @Test + public void blockCapsuleSanitizeStripsBlockHeaderOuterUnknownFields() { + BlockHeader paddedHeader = BlockHeader.newBuilder() + .setRawData(sampleRawHeader()) + .setUnknownFields(PADDING) + .build(); + Block padded = Block.newBuilder().setBlockHeader(paddedHeader).build(); + BlockCapsule capsule = new BlockCapsule(padded); + long originalSize = capsule.getData().length; + + assertTrue("sanitize() should report it mutated the capsule", capsule.sanitize()); + + assertTrue("BlockHeader outer unknown fields should be stripped", + capsule.getInstance().getBlockHeader().getUnknownFields().asMap().isEmpty()); + assertTrue(capsule.getData().length < originalSize); + } + + @Test + public void blockCapsuleSanitizePreservesBlockHeaderRawData() { + Block clean = sampleBlock(); + Block padded = clean.toBuilder().setUnknownFields(PADDING).build(); + BlockCapsule capsule = new BlockCapsule(padded); + + capsule.sanitize(); + + assertEquals("BlockHeader.raw_data must be byte-identical so block hash matches", + clean.getBlockHeader().getRawData(), + capsule.getInstance().getBlockHeader().getRawData()); + } + + @Test + public void blockCapsuleSanitizeIsNoOpOnCleanBlock() { + Block clean = sampleBlock(); + BlockCapsule capsule = new BlockCapsule(clean); + Block beforeInstance = capsule.getInstance(); + byte[] beforeData = capsule.getData(); + + assertFalse("sanitize() should report no-op on a clean block", capsule.sanitize()); + + assertSame("Underlying Block reference should not be rebuilt", + beforeInstance, capsule.getInstance()); + assertArrayEquals("Clean block should pass through unchanged", beforeData, capsule.getData()); + } + + // ---- TransactionCapsule.sanitize ---- + + @Test + public void transactionCapsuleSanitizeStripsTopLevelUnknownFields() { + Transaction padded = sampleTransaction().toBuilder().setUnknownFields(PADDING).build(); + TransactionCapsule capsule = new TransactionCapsule(padded); + long originalSize = capsule.getData().length; + + assertTrue("sanitize() should report it mutated the capsule", capsule.sanitize()); + + assertTrue("Transaction-level unknown fields should be stripped", + capsule.getInstance().getUnknownFields().asMap().isEmpty()); + assertTrue(capsule.getData().length < originalSize); + } + + @Test + public void transactionCapsuleSanitizePreservesTransactionId() { + Transaction clean = sampleTransaction(); + Transaction padded = clean.toBuilder().setUnknownFields(PADDING).build(); + TransactionCapsule cleanCapsule = new TransactionCapsule(clean); + TransactionCapsule paddedCapsule = new TransactionCapsule(padded); + + paddedCapsule.sanitize(); + + assertEquals("Padding outside raw_data must not change the transaction id", + cleanCapsule.getTransactionId(), + paddedCapsule.getTransactionId()); + } + + @Test + public void transactionCapsuleSanitizeIsNoOpOnCleanTransaction() { + Transaction clean = sampleTransaction(); + TransactionCapsule capsule = new TransactionCapsule(clean); + Transaction beforeInstance = capsule.getInstance(); + byte[] beforeData = capsule.getData(); + + assertFalse("sanitize() should report no-op on a clean transaction", capsule.sanitize()); + + assertSame("Underlying Transaction reference should not be rebuilt", + beforeInstance, capsule.getInstance()); + assertArrayEquals(beforeData, capsule.getData()); + } + + // ---- BlockMessage.sanitize ---- + + @Test + public void blockMessageSanitizeUpdatesBothCapsuleAndWireBytes() throws Exception { + Block padded = sampleBlock().toBuilder().setUnknownFields(PADDING).build(); + byte[] paddedBytes = padded.toByteArray(); + BlockMessage msg = new BlockMessage(paddedBytes); + assertArrayEquals("Constructor should not sanitize on its own", + paddedBytes, msg.getData()); + + msg.sanitize(); + + assertTrue("BlockCapsule should be sanitized", + msg.getBlockCapsule().getInstance().getUnknownFields().asMap().isEmpty()); + assertTrue("msg.data should also be rewritten to canonical bytes", + msg.getData().length < paddedBytes.length); + assertArrayEquals("msg.data should equal capsule.getData() after sanitize", + msg.getBlockCapsule().getData(), msg.getData()); + assertNotEquals("msg.data should no longer match the padded wire bytes", + paddedBytes.length, msg.getData().length); + } + + @Test + public void blockMessageSanitizeSkipsDataRewriteOnCleanBlock() throws Exception { + byte[] cleanBytes = sampleBlock().toByteArray(); + BlockMessage msg = new BlockMessage(cleanBytes); + byte[] before = msg.getData(); + + msg.sanitize(); + + assertSame("msg.data should not be rewritten on the no-op path", + before, msg.getData()); + } +} diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java index dab76cfcb46..56853c3dbb7 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java @@ -3,11 +3,15 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.utils.Pair; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.Parameter.NetConstants; +import org.tron.core.config.args.Args; import org.tron.core.exception.P2pException; import org.tron.core.net.message.keepalive.PingMessage; import org.tron.core.net.message.sync.ChainInventoryMessage; @@ -15,6 +19,16 @@ public class ChainInventoryMsgHandlerTest { + @BeforeClass + public static void init() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + private ChainInventoryMsgHandler handler = new ChainInventoryMsgHandler(); private PeerConnection peer = new PeerConnection(); private ChainInventoryMessage msg = new ChainInventoryMessage(new ArrayList<>(), 0L); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java index e8ec4257814..7ea6337443d 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java @@ -156,6 +156,27 @@ public void testDuplicateHashRejected() throws Exception { } } + @Test + public void testUnknownInventoryTypeRejected() throws Exception { + FetchInvDataMsgHandler handler = new FetchInvDataMsgHandler(); + PeerConnection peer = Mockito.mock(PeerConnection.class); + + // craft a FetchInvData whose enum type holds an undefined value (2) + Protocol.Inventory inv = Protocol.Inventory.newBuilder() + .setTypeValue(2) + .addIds(com.google.protobuf.ByteString.copyFrom(new byte[32])) + .build(); + FetchInvDataMessage msg = new FetchInvDataMessage(inv.toByteArray()); + Assert.assertEquals(Protocol.Inventory.InventoryType.UNRECOGNIZED, msg.getInventoryType()); + + try { + handler.processMessage(peer, msg); + Assert.fail("Expected P2pException for unknown inventory type"); + } catch (P2pException e) { + Assert.assertEquals(P2pException.TypeEnum.BAD_MESSAGE, e.getType()); + } + } + @Test public void testRateLimiter() { List blockIds = new LinkedList<>(); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java index 3d24ff2a4bf..9e2523dff2d 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.mock; +import com.google.protobuf.ByteString; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -18,6 +19,7 @@ import org.tron.core.net.message.adv.InventoryMessage; import org.tron.core.net.peer.PeerConnection; import org.tron.p2p.connection.Channel; +import org.tron.protos.Protocol.Inventory; import org.tron.protos.Protocol.Inventory.InventoryType; public class InventoryMsgHandlerTest { @@ -74,6 +76,30 @@ public void testDuplicateHashesRejected() throws Exception { } } + @Test + public void testUnknownInventoryTypeRejected() throws Exception { + InventoryMsgHandler handler = new InventoryMsgHandler(); + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + + // craft an Inventory whose enum type holds an undefined value (2) + Inventory inv = Inventory.newBuilder() + .setTypeValue(2) + .addIds(ByteString.copyFrom(new byte[32])) + .build(); + InventoryMessage msg = new InventoryMessage(inv.toByteArray()); + Assert.assertEquals(InventoryType.UNRECOGNIZED, msg.getInventoryType()); + + PeerConnection peer = new PeerConnection(); + peer.setChannel(getChannel("1.0.0.5", 1000)); + + try { + handler.processMessage(peer, msg); + Assert.fail("Expected P2pException for unknown inventory type"); + } catch (P2pException e) { + Assert.assertEquals(P2pException.TypeEnum.BAD_MESSAGE, e.getType()); + } + } + private Channel getChannel(String host, int port) throws Exception { Channel channel = new Channel(); InetSocketAddress inetSocketAddress = new InetSocketAddress(host, port); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java index abe69e76ff2..ed2121d360f 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java @@ -337,6 +337,91 @@ public void testDuplicateTransactionRejected() throws Exception { } } + @Test + public void testInvalidSigLength() throws Exception { + TransactionsMsgHandler handler = new TransactionsMsgHandler(); + handler.init(); + try { + PeerConnection peer = Mockito.mock(PeerConnection.class); + + BalanceContract.TransferContract transferContract = BalanceContract.TransferContract + .newBuilder() + .setAmount(10) + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString("121212a9cf"))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString("232323a9cf"))) + .build(); + + // signature shorter than 65 bytes → BAD_TRX + Protocol.Transaction shortSigTrx = Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .addContract(Protocol.Transaction.Contract.newBuilder() + .setType(Protocol.Transaction.Contract.ContractType.TransferContract) + .setParameter(Any.pack(transferContract)).build()) + .build()) + .addSignature(ByteString.copyFrom(new byte[64])) + .build(); + + List shortList = new ArrayList<>(); + shortList.add(shortSigTrx); + stubAdvInvRequest(peer, new TransactionsMessage(shortList)); + P2pException shortEx = Assert.assertThrows(P2pException.class, + () -> handler.processMessage(peer, new TransactionsMessage(shortList))); + Assert.assertEquals(TypeEnum.BAD_TRX, shortEx.getType()); + + // signature longer than 68 bytes → BAD_TRX + Protocol.Transaction longSigTrx = Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .setRefBlockNum(1) + .addContract(Protocol.Transaction.Contract.newBuilder() + .setType(Protocol.Transaction.Contract.ContractType.TransferContract) + .setParameter(Any.pack(transferContract)).build()) + .build()) + .addSignature(ByteString.copyFrom(new byte[69])) + .build(); + + List longList = new ArrayList<>(); + longList.add(longSigTrx); + stubAdvInvRequest(peer, new TransactionsMessage(longList)); + P2pException longEx = Assert.assertThrows(P2pException.class, + () -> handler.processMessage(peer, new TransactionsMessage(longList))); + Assert.assertEquals(TypeEnum.BAD_TRX, longEx.getType()); + + // exactly 65 bytes → passes the length check (no P2pException from check) + Protocol.Transaction validSigTrx = Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .setRefBlockNum(2) + .addContract(Protocol.Transaction.Contract.newBuilder() + .setType(Protocol.Transaction.Contract.ContractType.TransferContract) + .setParameter(Any.pack(transferContract)).build()) + .build()) + .addSignature(ByteString.copyFrom(new byte[65])) + .build(); + + List validList = new ArrayList<>(); + validList.add(validSigTrx); + stubAdvInvRequest(peer, new TransactionsMessage(validList)); + handler.processMessage(peer, new TransactionsMessage(validList)); + + // 68 bytes (upper bound) also passes the length check + Protocol.Transaction paddedSigTrx = Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .setRefBlockNum(3) + .addContract(Protocol.Transaction.Contract.newBuilder() + .setType(Protocol.Transaction.Contract.ContractType.TransferContract) + .setParameter(Any.pack(transferContract)).build()) + .build()) + .addSignature(ByteString.copyFrom(new byte[68])) + .build(); + + List paddedList = new ArrayList<>(); + paddedList.add(paddedSigTrx); + stubAdvInvRequest(peer, new TransactionsMessage(paddedList)); + handler.processMessage(peer, new TransactionsMessage(paddedList)); + } finally { + handler.close(); + } + } + @Test public void testIsBusyWithCachedTransactions() throws Exception { TransactionsMsgHandler handler = new TransactionsMsgHandler(); diff --git a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java index 8585244b941..7c28757bd5c 100644 --- a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java @@ -220,6 +220,30 @@ private void testCheckHelloMessage() { boolean res = service.checkHelloMessage(helloMessage, c1); Assert.assertTrue(res); + + HelloMessage shortSigMsg = new HelloMessage(node, System.currentTimeMillis(), + ChainBaseManager.getChainBaseManager()); + shortSigMsg.setHelloMessage(shortSigMsg.getHelloMessage().toBuilder() + .setAddress(address) + .setSignature(ByteString.copyFrom(new byte[64])) + .build()); + Assert.assertFalse(service.checkHelloMessage(shortSigMsg, c1)); + + HelloMessage longSigMsg = new HelloMessage(node, System.currentTimeMillis(), + ChainBaseManager.getChainBaseManager()); + longSigMsg.setHelloMessage(longSigMsg.getHelloMessage().toBuilder() + .setAddress(address) + .setSignature(ByteString.copyFrom(new byte[69])) + .build()); + Assert.assertFalse(service.checkHelloMessage(longSigMsg, c1)); + + HelloMessage emptySigMsg = new HelloMessage(node, System.currentTimeMillis(), + ChainBaseManager.getChainBaseManager()); + emptySigMsg.setHelloMessage(emptySigMsg.getHelloMessage().toBuilder() + .setAddress(address) + .setSignature(ByteString.EMPTY) + .build()); + Assert.assertFalse(service.checkHelloMessage(emptySigMsg, c1)); } catch (Exception e) { logger.info("", e); assert false; diff --git a/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java new file mode 100644 index 00000000000..b293e8047b5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/HttpInterceptorTest.java @@ -0,0 +1,44 @@ +package org.tron.core.services.filter; + +import static org.junit.Assert.assertThrows; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +public class HttpInterceptorTest { + + private final HttpInterceptor interceptor = new HttpInterceptor(); + + @Test + public void testOversizedBadMessagePropagates() { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc"); + request.setServletPath("/jsonrpc"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = (req, resp) -> { + throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, + "Request body is too large"); + }; + + BadMessageException e = assertThrows(BadMessageException.class, + () -> interceptor.doFilter(request, response, chain)); + + org.junit.Assert.assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, e.getCode()); + } + + @Test + public void testNonOversizedExceptionIsStillSwallowed() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/jsonrpc"); + request.setServletPath("/jsonrpc"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = (req, resp) -> { + throw new ServletException("expected"); + }; + + interceptor.doFilter(request, response, chain); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java index 83fb64880c3..703f278c890 100644 --- a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java @@ -55,4 +55,31 @@ && addressEquals(((CreateSmartContract) c).getOwnerAddress(), eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); assertTransactionResponse(response); } + + @Test + public void testDeployContractOmitsNullAbiOutputs() throws Exception { + String jsonParam = "{" + + "\"owner_address\": \"4199357684BC659F5166046B56C95A0E99F1265CD1\"," + + "\"name\": \"TestContract\"," + + "\"abi\": [{\"inputs\":[],\"name\":\"test\",\"outputs\":null," + + "\"type\":\"function\"}]," + + "\"bytecode\": \"608060405234801561001057600080fd5b50\"," + + "\"fee_limit\": 1000000000," + + "\"call_value\": 0," + + "\"consume_user_resource_percent\": 100," + + "\"origin_energy_limit\": 10000000" + + "}"; + MockHttpServletRequest request = postRequest(jsonParam); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).createTransactionCapsule( + argThat(c -> c instanceof CreateSmartContract + && ((CreateSmartContract) c).getNewContract().getAbi().getEntrysCount() == 1 + && ((CreateSmartContract) c).getNewContract().getAbi().getEntrys(0) + .getOutputsCount() == 0), + eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); + assertTransactionResponse(response); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java index 466917d0cd5..1c1d42c9a5c 100644 --- a/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/GetAccountServletTest.java @@ -14,6 +14,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.tron.common.crypto.ECKey; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.StringUtil; import org.tron.protos.Protocol.Account; public class GetAccountServletTest extends BaseHttpTest { @@ -45,6 +46,20 @@ public void testGetAccountPost() throws Exception { assertTrue("Should contain address", content.contains("address")); } + @Test + public void testGetAccountPostNullAddressKeepsDefault() throws Exception { + MockHttpServletRequest request = postRequest("{\"address\": null}"); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).getAccount(argThat(req -> req != null + && req.getAddress().equals(ByteString.EMPTY))); + String content = response.getContentAsString(); + assertFalse("Should not contain error", content.contains("\"Error\"")); + assertTrue("Should contain address", content.contains("address")); + } + @Test public void testGetAccountGet() throws Exception { MockHttpServletRequest request = getRequest("address", addrStr); @@ -57,4 +72,35 @@ public void testGetAccountGet() throws Exception { assertFalse("Should not contain error", content.contains("\"Error\"")); assertTrue("Should contain address", content.contains("address")); } + + @Test + public void testGetAccountPostOversizedBase58Address() throws Exception { + String oversized = addrStr + "0"; + String jsonParam = "{\"address\": \"" + oversized + "\", \"visible\": true}"; + MockHttpServletRequest request = postRequest(jsonParam); + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + String content = response.getContentAsString(); + // null from decodeFromBase58Check now surfaces a clear "invalid address" error instead of a + // bare NullPointerException (JsonFormat checks for null before ByteString.copyFrom). + assertTrue("oversized address should surface a clear invalid-address error: " + content, + content.contains("invalid address")); + } + + @Test + public void testGetAccountPostVisibleBase58Address() throws Exception { + // visible=true happy path: a valid 34-char Base58 address decodes back to the same 21 bytes + // (decodeFromBase58Check success path) and returns the account with no error. + String base58Addr = StringUtil.encode58Check(address); + String jsonParam = "{\"address\": \"" + base58Addr + "\", \"visible\": true}"; + MockHttpServletRequest request = postRequest(jsonParam); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(request, response); + assertEquals(200, response.getStatus()); + verify(wallet).getAccount(argThat(req -> req != null && req.getAddress().equals(addr))); + String content = response.getContentAsString(); + assertFalse("Should not contain error", content.contains("\"Error\"")); + assertTrue("Should contain address", content.contains("address")); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java b/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java index 76f85da5d8f..9afa5607a66 100644 --- a/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/GetRewardServletTest.java @@ -130,4 +130,35 @@ public void getByBlankParamTest() { } } + @Test + public void getRewardByOversizedValidCharAddressTest() { + // 41-char, all-valid-Base58 address: the length guard returns null -> reward 0. + MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("address", "T" + new String(new char[40]).replace('\0', 'a')); + new GetRewardServlet().doPost(request, response); + try { + JSONObject result = JSONObject.parseObject(response.getContentAsString()); + Assert.assertEquals(0, (int) result.get("reward")); + Assert.assertNull(result.get("Error")); + } catch (UnsupportedEncodingException e) { + Assert.fail(e.getMessage()); + } + } + + @Test + public void getRewardByOversizedIllegalCharAddressTest() { + MockHttpServletRequest request = createRequest("application/x-www-form-urlencoded"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("address", "T" + new String(new char[40]).replace('\0', '0')); + new GetRewardServlet().doPost(request, response); + try { + JSONObject result = JSONObject.parseObject(response.getContentAsString()); + Assert.assertEquals(0, (int) result.get("reward")); + Assert.assertNull(result.get("Error")); + } catch (UnsupportedEncodingException e) { + Assert.fail(e.getMessage()); + } + } + } diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java index a8525b0f526..46d1743c5b9 100644 --- a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java @@ -1,6 +1,7 @@ package org.tron.core.services.http; 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; @@ -17,7 +18,11 @@ import org.junit.After; import org.junit.Test; import org.mockito.Mockito; +import org.tron.core.Constant; import org.tron.protos.Protocol; +import org.tron.protos.contract.ProposalContract.ProposalCreateContract; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; public class JsonFormatTest { @After @@ -252,4 +257,214 @@ public void testParseInteger() throws Exception { assertTrue(cause instanceof NumberFormatException); } -} \ No newline at end of file + /* + * Compatibility-preserved behavior: these cases pass before and after this fix. + * They guard unknown-field skipping, repeated-field syntax, and accepted depth. + */ + @Test + public void testMissingFieldSkipsNestedObjectAndArray() throws Exception { + String input = "{\"zzz\":{\"a\":1,\"b\":[true,false,null,{\"c\":\"d\"}],\"e\":{\"f\":2}}," + + "\"address\":\"61646472657373\"}"; + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.merge(input, builder, false); + + assertEquals(ByteString.copyFromUtf8("address"), builder.getAddress()); + } + + @Test + public void testTrailingCommaInKnownRepeatedField() throws Exception { + Protocol.Proposal.Builder builder = Protocol.Proposal.newBuilder(); + + JsonFormat.merge("{\"approvals\":[\"00\",\"01\",]}", builder, false); + + Protocol.Proposal proposal = builder.build(); + assertEquals(2, proposal.getApprovalsCount()); + assertEquals(ByteString.copyFrom(new byte[] {0}), proposal.getApprovals(0)); + assertEquals(ByteString.copyFrom(new byte[] {1}), proposal.getApprovals(1)); + } + + @Test + public void testKnownFieldNullIsSkipped() throws Exception { + Protocol.HelloMessage.Builder hello = Protocol.HelloMessage.newBuilder(); + JsonFormat.merge("{\"address\":null}", hello, false); + assertEquals(ByteString.EMPTY, hello.getAddress()); + + Entry.Builder entry = Entry.newBuilder(); + JsonFormat.merge("{\"outputs\":null}", entry, false); + assertEquals(0, entry.getOutputsCount()); + + CreateSmartContract.Builder contract = CreateSmartContract.newBuilder(); + JsonFormat.merge("{\"new_contract\":null}", contract, false); + assertFalse(contract.hasNewContract()); + + ProposalCreateContract.Builder proposal = ProposalCreateContract.newBuilder(); + JsonFormat.merge("{\"parameters\":null}", proposal, false); + assertTrue(proposal.getParametersMap().isEmpty()); + } + + @Test + public void testKnownRepeatedPrimitiveFieldRejectsNestedArray() { + Protocol.Proposal.Builder builder = Protocol.Proposal.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"approvals\":[[\"00\"]]}", builder, false)); + + assertTrue(e.getMessage().contains("Expected string.")); + } + + @Test + public void testKnownRepeatedMessageFieldRejectsNestedArray() { + Protocol.Block.Builder builder = Protocol.Block.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"transactions\":[[]]}", builder, false)); + + assertTrue(e.getMessage().contains("Expected \"{\".")); + } + + @Test + public void testKnownRepeatedMessageFieldRejectsNullElement() { + Entry.Builder builder = Entry.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"outputs\":[null]}", builder, false)); + + assertTrue(e.getMessage().contains("Expected \"{\".")); + } + + @Test + public void testMissingFieldRejectsTrailingCommaInNestedObject() { + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"zzz\":{\"a\":1,}}", builder)); + + assertTrue(e.getMessage().contains("Expected identifier")); + } + + @Test + public void testMissingFieldRejectsTrailingCommaInNestedArray() { + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"zzz\":[1,]}", builder)); + + assertTrue(e.getMessage().contains("Expected string.")); + } + + @Test + public void testNestingWithinLimit() throws Exception { + int depth = Constant.MAX_NESTING_DEPTH / 2; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append("{\"zzz\":"); + } + sb.append("1"); + for (int i = 0; i < depth; i++) { + sb.append("}"); + } + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + JsonFormat.merge(sb.toString(), builder); + assertNotNull(builder.build()); + } + + /* + * Behavior changed by this fix: the previous parser either missed the depth guard + * or failed through recursive comma handling / stack overflow. + */ + @Test + public void testTrailingCommaInParsedObjects() throws Exception { + String input = "{\"genesisBlockId\":{\"hash\":\"00\",\"number\":1,}," + + "\"address\":\"61646472657373\",}"; + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.merge(input, builder, false); + + Protocol.HelloMessage message = builder.build(); + assertEquals(ByteString.copyFrom(new byte[] {0}), message.getGenesisBlockId().getHash()); + assertEquals(1L, message.getGenesisBlockId().getNumber()); + assertEquals(ByteString.copyFromUtf8("address"), message.getAddress()); + } + + @Test + public void testMissingFieldRejectsObjectBeyondNestingLimit() { + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(buildUnknownNestedObject(Constant.MAX_NESTING_DEPTH + 1), builder)); + + assertTrue(e.getMessage().contains("Hit recursion limit.")); + } + + @Test + public void testMissingFieldRejectsArrayBeyondNestingLimit() { + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + + JsonFormat.ParseException e = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge(buildUnknownNestedArray(Constant.MAX_NESTING_DEPTH + 1), builder)); + + assertTrue(e.getMessage().contains("Hit recursion limit.")); + } + + @Test + public void testDeeplyNestedObject() { + int depth = 100_000; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append("{\"zzz\":"); + } + sb.append("1"); + for (int i = 0; i < depth; i++) { + sb.append("}"); + } + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + assertThrows(JsonFormat.ParseException.class, () -> JsonFormat.merge(sb.toString(), builder)); + } + + @Test + public void testDeeplyNestedArray() { + int depth = 100_000; + StringBuilder sb = new StringBuilder(); + sb.append("{\"zzz\":"); + for (int i = 0; i < depth; i++) { + sb.append("["); + } + sb.append("1"); + for (int i = 0; i < depth; i++) { + sb.append("]"); + } + sb.append("}"); + Protocol.HelloMessage.Builder builder = Protocol.HelloMessage.newBuilder(); + assertThrows(JsonFormat.ParseException.class, () -> JsonFormat.merge(sb.toString(), builder)); + } + + private String buildUnknownNestedObject(int depth) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (int i = 0; i < depth; i++) { + sb.append("\"zzz\":{"); + } + sb.append("\"leaf\":1"); + for (int i = 0; i < depth; i++) { + sb.append("}"); + } + sb.append("}"); + return sb.toString(); + } + + private String buildUnknownNestedArray(int depth) { + StringBuilder sb = new StringBuilder(); + sb.append("{\"zzz\":"); + for (int i = 0; i < depth; i++) { + sb.append("["); + } + sb.append("1"); + for (int i = 0; i < depth; i++) { + sb.append("]"); + } + sb.append("}"); + return sb.toString(); + } + +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java index 1ae341696eb..26826c5709d 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -16,6 +16,8 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; @@ -69,6 +71,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) { } } + static class OversizedRequestServlet extends RateLimiterServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, + "Request body is too large"); + } + } + /** * GlobalRateLimiter's static initializer calls Args.getInstance().getRateLimiterGlobalQps(). * Without Args being initialized the default QPS is 0, causing RateLimiter.create(0) to throw. @@ -167,14 +177,14 @@ public void testBuildsEachWhitelistedAdapter() { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); - // tryAcquire returned false — no permit was taken, nothing to release + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); + // acquirePermit returned false — no permit was taken, nothing to release verify(perEndpoint, never()).release(); } } @@ -186,13 +196,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws Exception { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -203,11 +213,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws E @Test public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); servlet.service(request, response); @@ -223,11 +233,11 @@ public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { @Test public void testBothPassPermitReleasedAfterRequest() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); @@ -243,11 +253,31 @@ public void testBothPassPermitReleasedAfterRequest() throws Exception { public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { // No entry added to container — container.get() returns null try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); + } + } + + @Test + public void testOversizedRequestBadMessagePropagates() throws Exception { + OversizedRequestServlet oversizedServlet = new OversizedRequestServlet(); + Field f = RateLimiterServlet.class.getDeclaredField("container"); + f.setAccessible(true); + f.set(oversizedServlet, container); + MockHttpServletRequest postRequest = new MockHttpServletRequest("POST", "/jsonrpc"); + postRequest.setRemoteAddr("10.0.0.1"); + postRequest.setServletPath("/jsonrpc"); + + try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); + + BadMessageException e = assertThrows(BadMessageException.class, + () -> oversizedServlet.service(postRequest, response)); + + assertEquals(HttpStatus.PAYLOAD_TOO_LARGE_413, e.getCode()); } } } diff --git a/framework/src/test/java/org/tron/core/services/http/TriggerConstantContractServletTest.java b/framework/src/test/java/org/tron/core/services/http/TriggerConstantContractServletTest.java new file mode 100644 index 00000000000..2a139f8a158 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/TriggerConstantContractServletTest.java @@ -0,0 +1,49 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.common.crypto.ECKey; +import org.tron.common.utils.ByteArray; +import org.tron.core.capsule.TransactionCapsule; + +public class TriggerConstantContractServletTest extends BaseHttpTest { + + private TriggerConstantContractServlet servlet; + + @Override + protected void setUpMocks() throws Exception { + servlet = new TriggerConstantContractServlet(); + injectWallet(servlet); + } + + @Test + public void testManyFlatFieldsDoesNotOverflowStack() throws Exception { + String owner = ByteArray.toHexString(new ECKey().getAddress()); + String contract = ByteArray.toHexString(new ECKey().getAddress()); + + StringBuilder body = new StringBuilder(256 * 1024) + .append("{\"owner_address\":\"").append(owner).append('"') + .append(",\"contract_address\":\"").append(contract).append('"') + .append(",\"data\":\"00\""); + for (int i = 0; i < 20_000; i++) { + body.append(",\"x").append(i).append("\":1"); + } + body.append('}'); + + when(wallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(MINIMAL_TX)); + when(wallet.triggerConstantContract(any(), any(), any(), any())) + .thenReturn(MINIMAL_TX); + + MockHttpServletResponse response = newResponse(); + servlet.doPost(postRequest(body.toString()), response); + + assertEquals(200, response.getStatus()); + verify(wallet).triggerConstantContract(any(), any(), any(), any()); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index ebcb530bca3..c619fd0de54 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -14,8 +14,10 @@ import org.tron.core.capsule.AccountCapsule; import org.tron.core.config.args.Args; import org.tron.core.utils.TransactionUtil; +import org.tron.json.JSONObject; import org.tron.protos.Protocol; import org.tron.protos.Protocol.Transaction; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; public class UtilTest extends BaseTest { @@ -189,4 +191,114 @@ public void testPackTransaction() { TransactionSignWeight txSignWeight = transactionUtil.getTransactionSignWeight(transaction); Assert.assertNotNull(txSignWeight); } + + @Test + public void testPackCreateSmartContractOmitsNullAbiOutputs() throws Exception { + String strTransaction = "{\n" + + " \"visible\": false,\n" + + " \"raw_data\": {\n" + + " \"contract\": [\n" + + " {\n" + + " \"parameter\": {\n" + + " \"value\": {\n" + + " \"owner_address\":\"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + " \"new_contract\": {\n" + + " \"origin_address\":" + + " \"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + " \"name\":\"TestContract\"," + + " \"abi\": {\n" + + " \"entrys\": [\n" + + " {\"inputs\":[],\"name\":\"test\"," + + " \"outputs\":null,\"type\":\"function\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"type_url\":" + + " \"type.googleapis.com/protocol.CreateSmartContract\"\n" + + " },\n" + + " \"type\": \"CreateSmartContract\"\n" + + " }\n" + + " ],\n" + + " \"ref_block_bytes\": \"d8ed\",\n" + + " \"ref_block_hash\": \"2e066c3259e756f5\",\n" + + " \"expiration\": 1651906644000,\n" + + " \"timestamp\": 1651906586162\n" + + " }\n" + + "}"; + + Transaction transaction = Util.packTransaction(strTransaction, false); + Assert.assertNotNull(transaction); + Assert.assertEquals(1, transaction.getRawData().getContractCount()); + CreateSmartContract contract = transaction.getRawData().getContract(0) + .getParameter().unpack(CreateSmartContract.class); + Assert.assertEquals(1, contract.getNewContract().getAbi().getEntrysCount()); + Assert.assertEquals(0, contract.getNewContract().getAbi().getEntrys(0).getOutputsCount()); + } + + private Transaction buildTooManySigsTransaction() { + String strTransaction = "{\n" + + " \"visible\": false,\n" + + " \"txID\": \"fc33817936b06e50d4b6f1797e62f52d69af6c0da580a607241a9c03a48e390e\",\n" + + " \"raw_data\": {\n" + + " \"contract\": [\n" + + " {\n" + + " \"parameter\": {\n" + + " \"value\": {\n" + + " \"amount\": 10,\n" + + " \"owner_address\":\"41c076305e35aea1fe45a772fcaaab8a36e87bdb55\"," + + " \"to_address\": \"415624c12e308b03a1a6b21d9b86e3942fac1ab92b\"\n" + + " },\n" + + " \"type_url\": \"type.googleapis.com/protocol.TransferContract\"\n" + + " },\n" + + " \"type\": \"TransferContract\"\n" + + " }\n" + + " ],\n" + + " \"ref_block_bytes\": \"d8ed\",\n" + + " \"ref_block_hash\": \"2e066c3259e756f5\",\n" + + " \"expiration\": 1651906644000,\n" + + " \"timestamp\": 1651906586162\n" + + " },\n" + + " \"raw_data_hex\": \"0a02d8ed22082e066c3259e756f540a090bcea89305a65080112610a2d747970" + + "652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e74726163741230" + + "0a1541c076305e35aea1fe45a772fcaaab8a36e87bdb551215415624c12e308b03a1a6b21d9b86e3942fac1a" + + "b92b180a70b2ccb8ea8930\"\n" + + "}"; + Transaction transaction = Util.packTransaction(strTransaction, false); + int totalSignNum = dbManager.getDynamicPropertiesStore().getTotalSignNum(); + ByteString dummySig = ByteString.copyFrom(new byte[65]); + Transaction.Builder builder = transaction.toBuilder(); + for (int i = 0; i < totalSignNum + 1; i++) { + builder.addSignature(dummySig); + } + return builder.build(); + } + + @Test + public void testPrintApprovedListTooManySigsHttpPath() { + Transaction transaction = buildTooManySigsTransaction(); + TransactionApprovedList reply = wallet.getTransactionApprovedList(transaction); + Assert.assertEquals(TransactionApprovedList.Result.response_code.OTHER_ERROR, + reply.getResult().getCode()); + // The early-return reply has no transaction; the HTTP print helper must not throw. + String json = Util.printTransactionApprovedList(reply, false); + JSONObject jsonObject = JSONObject.parseObject(json); + Assert.assertNull(jsonObject.getJSONObject("transaction")); + Assert.assertTrue(jsonObject.getJSONObject("result").getString("message") + .contains("too many signatures")); + } + + @Test + public void testPrintSignWeightTooManySigsHttpPath() { + Transaction transaction = buildTooManySigsTransaction(); + TransactionSignWeight reply = transactionUtil.getTransactionSignWeight(transaction); + Assert.assertEquals(TransactionSignWeight.Result.response_code.OTHER_ERROR, + reply.getResult().getCode()); + // The early-return reply has no transaction; the HTTP print helper must not throw. + String json = Util.printTransactionSignWeight(reply, false); + JSONObject jsonObject = JSONObject.parseObject(json); + Assert.assertNull(jsonObject.getJSONObject("transaction")); + Assert.assertTrue(jsonObject.getJSONObject("result").getString("message") + .contains("too many signatures")); + } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 753d93d47f4..3f2a91bea1c 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -69,6 +69,16 @@ public void testParseGas() throws JsonRpcInvalidParamsException { Assert.assertEquals(16L, gas); } + @Test + public void parseValueRejectsNegative() { + // QUANTITY is unsigned; a signed "0x-.." value must be rejected, not returned as negative. + BuildArguments args = new BuildArguments(); + args.setValue("0x-1"); + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> args.parseValue()); + Assert.assertEquals("invalid param value: negative", e.getMessage()); + } + @Test public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { BuildArguments args = new BuildArguments(); diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java index 6aaeea2cc4e..f719446df72 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -49,8 +49,8 @@ public void parseBlockNumberRejectsOverflow() { @Test public void parseBlockNumberRejectsOversized() { - // 101 chars exceeds the 100-char limit - String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + // 21 chars exceeds the 20-char limit + String tooLong = "0x" + new String(new char[18]).replace('\0', '0') + "1"; JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); assertEquals("invalid block number", e.getMessage()); @@ -75,4 +75,145 @@ public void parseBlockNumberRejectsEmpty() { assertThrows(JsonRpcInvalidParamsException.class, () -> JsonRpcApiUtil.parseBlockNumber("")); } + + @Test + public void addressCompatibleToByteArrayNormal() + throws JsonRpcInvalidParamsException { + String addr = "0xabd4b9367799eaa3197fecb144eb71de1e049abc"; + assertEquals(21, JsonRpcApiUtil.addressCompatibleToByteArray(addr).length); + } + + @Test + public void addressCompatibleToByteArrayRejectsOversized() { + // 45 chars + String justOver = "0x" + new String(new char[43]).replace('\0', 'a'); + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.addressCompatibleToByteArray(justOver)); + assertEquals("invalid address", e1.getMessage()); + } + + @Test + public void addressCompatibleToByteArrayRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.addressCompatibleToByteArray(null)); + assertEquals("invalid address", e.getMessage()); + } + + @Test + public void hashToByteArrayAcceptsValidHex() throws JsonRpcInvalidParamsException { + String hash = "0x" + new String(new char[64]).replace('\0', 'a'); + assertEquals(32, JsonRpcApiUtil.hashToByteArray(hash).length); + } + + @Test + public void hashToByteArrayRejectsNonHexChars() { + String hash = "0x" + new String(new char[64]).replace('\0', 'g'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.hashToByteArray(hash)); + assertEquals("invalid hash value", e.getMessage()); + } + + @Test + public void topicToByteArrayAcceptsFullLengthHex() throws JsonRpcInvalidParamsException { + String topic = "0x" + new String(new char[64]).replace('\0', 'a'); + assertEquals(32, JsonRpcApiUtil.topicToByteArray(topic).length); + } + + @Test + public void topicToByteArrayPadsMissingLeadingZero() throws JsonRpcInvalidParamsException { + String stripped = "0x" + new String(new char[63]).replace('\0', 'a'); + byte[] parsed = JsonRpcApiUtil.topicToByteArray(stripped); + assertEquals(32, parsed.length); + assertEquals(0x0a, parsed[0]); + } + + @Test + public void topicToByteArrayRejectsNonHexChars() { + String topic = "0x" + new String(new char[64]).replace('\0', 'g'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(topic)); + assertEquals("invalid topic: " + topic, e.getMessage()); + } + + @Test + public void topicToByteArrayRejectsWrongLength() { + // 62 chars (two zeros stripped) and 65 chars are both invalid + String tooShort = "0x" + new String(new char[62]).replace('\0', 'a'); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(tooShort)); + String tooLong = "0x" + new String(new char[65]).replace('\0', 'a'); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(tooLong)); + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.topicToByteArray(null)); + } + + @Test + public void parseTxIndexAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1a, JsonRpcApiUtil.parseTxIndex("0x1a")); + assertEquals(0, JsonRpcApiUtil.parseTxIndex("0x0")); + // 8 hex digits is the max width; 0x7fffffff is Integer.MAX_VALUE + assertEquals(Integer.MAX_VALUE, JsonRpcApiUtil.parseTxIndex("0x7fffffff")); + } + + @Test + public void parseTxIndexRejectsMissingPrefix() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("1a")); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexAcceptsLeadingZeros() throws JsonRpcInvalidParamsException { + // leading zeros are tolerated (only length is capped); "0x01"/"0x00" parse normally + assertEquals(1, JsonRpcApiUtil.parseTxIndex("0x01")); + assertEquals(0, JsonRpcApiUtil.parseTxIndex("0x00")); + } + + @Test + public void parseTxIndexRejectsOversized() { + // 9 hex digits exceeds the 8-digit (0x + 8) limit -> rejected before parsing + String tooLong = "0x" + new String(new char[9]).replace('\0', '1'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex(tooLong)); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexRejectsEmptyAndNull() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("0x")); + assertEquals("invalid index value", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex(null)); + assertEquals("invalid index value", e2.getMessage()); + } + + @Test + public void parseTxIndexRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseTxIndex("0xGG")); + assertEquals("invalid index value", e.getMessage()); + } + + @Test + public void parseTxIndexParsesNegativeForCallerRangeCheck() throws JsonRpcInvalidParamsException { + // "0x-1" is syntactically accepted and parsed to a negative int; the RPC handler maps + // any out-of-range index (negative or >= tx count) to a null result. + assertEquals(-1, JsonRpcApiUtil.parseTxIndex("0x-1")); + } + + @Test + public void calcFeeLimitNormal() throws JsonRpcInvalidParamsException { + assertEquals(8400L, JsonRpcApiUtil.calcFeeLimit(20L, 420L)); + assertEquals(0L, JsonRpcApiUtil.calcFeeLimit(0L, 420L)); + } + + @Test + public void calcFeeLimitRejectsOverflow() { + // gas * energyFee overflows int64 -> rejected instead of silently wrapping to a bogus feeLimit + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.calcFeeLimit(Long.MAX_VALUE, 420L)); + assertEquals("invalid gas: fee limit overflow", e.getMessage()); + } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index fa45ca48876..d6c843b5aea 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -25,6 +26,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.tron.common.parameter.CommonParameter; +import org.tron.core.Constant; public class JsonRpcServletTest { @@ -61,6 +63,8 @@ public void invalidJson_returnsParseError() throws Exception { JsonNode body = MAPPER.readTree(resp.getContentAsString()); assertFalse(body.isArray()); assertEquals(-32700, body.get("error").get("code").asInt()); + // A non-constraint JsonProcessingException keeps the generic message (else branch). + assertEquals("JSON parse error", body.get("error").get("message").asText()); assertEquals("2.0", body.get("jsonrpc").asText()); assertTrue(body.get("id").isNull()); } @@ -245,6 +249,172 @@ public void normalRequest_commitsRpcServerResponse() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + // --- Content-Type header: must be application/json-rpc (no charset suffix) --- + + @Test + public void errorResponse_contentTypeIsApplicationJsonRpc() throws Exception { + MockHttpServletResponse resp = doPost("not valid json"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void batchResponse_contentTypeIsApplicationJsonRpc() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}]"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void allNotificationBatch_contentTypeIsApplicationJsonRpc() throws Exception { + // notification: rpcServer returns 0 bytes → empty batchResult → early return path + doAnswer(inv -> 0).when(mockRpcServer) + .handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); + assertEquals(200, resp.getStatus()); + assertEquals(0, resp.getContentLength()); + assertEquals("application/json-rpc", resp.getContentType()); + } + + // --- Primitive root node → Invalid Request (-32600), id must be JSON null --- + + @Test + public void primitiveRootNull_returnsInvalidRequestWithJsonNullId() throws Exception { + MockHttpServletResponse resp = doPost("null"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue("id must be JSON null, not the string \"null\"", body.get("id").isNull()); + assertFalse("id must not be a string", body.get("id").isTextual()); + } + + @Test + public void primitiveRootBoolean_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("true"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootNumber_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("123"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootString_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("\"hello\""); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + // --- Non-object element inside a batch → Invalid Request per element --- + + @Test + public void batchWithNestedArray_returnsInvalidRequestArray() throws Exception { + MockHttpServletResponse resp = doPost("[[]]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(1, body.size()); + assertEquals(-32600, body.get(0).get("error").get("code").asInt()); + assertTrue("id in batch error must be JSON null", body.get(0).get("id").isNull()); + } + + @Test + public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}, []]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(2, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); + assertEquals(-32600, body.get(1).get("error").get("code").asInt()); + } + + @Test + public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[42, \"foo\", true]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(3, body.size()); + for (int i = 0; i < 3; i++) { + assertEquals(-32600, body.get(i).get("error").get("code").asInt()); + } + } + + // --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced --- + + @Test + public void excessivelyNestedRequest_returnsParseError() throws Exception { + int limit = Constant.MAX_NESTING_DEPTH; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i <= limit; i++) { + sb.append('['); + } + sb.append('0'); + for (int i = 0; i <= limit; i++) { + sb.append(']'); + } + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + JsonNode error = MAPPER.readTree(resp.getContentAsString()).get("error"); + assertEquals(-32700, error.get("code").asInt()); + // StreamConstraintsException message must be surfaced verbatim, not the generic text, + // so callers can tell which constraint (nesting depth) was hit. + String message = error.get("message").asText(); + assertNotEquals("JSON parse error", message); + assertTrue("expected a nesting-depth constraint message, got: " + message, + message.contains("nesting depth") && message.contains("exceeds the maximum allowed")); + } + + @Test + public void tooManyTokens_returnsParseError() throws Exception { + int limit = Constant.MAX_TOKEN_COUNT; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < limit; i++) { + if (i > 0) { + sb.append(','); + } + sb.append('0'); + } + sb.append(']'); + + MockHttpServletResponse resp = doPost(sb.toString()); + assertEquals(200, resp.getStatus()); + JsonNode error = MAPPER.readTree(resp.getContentAsString()).get("error"); + assertEquals(-32700, error.get("code").asInt()); + // StreamConstraintsException message must be surfaced verbatim, not the generic text, + // so callers can tell which constraint (token count) was hit. + String message = error.get("message").asText(); + assertNotEquals("JSON parse error", message); + assertTrue("expected a token-count constraint message, got: " + message, + message.contains("Token count") && message.contains("exceeds the maximum allowed")); + } + // --- helpers --- private MockHttpServletResponse doPost(String body) throws Exception { diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java index c34d49d9009..8ea0f908899 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java @@ -1,5 +1,10 @@ package org.tron.core.services.ratelimiter; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.RateLimiter; @@ -9,6 +14,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.tron.common.TestConstants; import org.tron.core.config.args.Args; @@ -135,6 +143,104 @@ public void testPerIpLimitsAreIndependent() throws Exception { Assert.assertFalse(GlobalRateLimiter.tryAcquire(runtimeDataFor("2.2.2.2"))); } + /** + * acquire() must drain the IP limiter before the global limiter, mirroring + * tryAcquire(). A reversed order would let one chatty IP consume global + * quota even when its own per-IP budget is exhausted. + */ + @Test + public void testAcquireOrdersIpBeforeGlobal() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + RateLimiter ipMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + Cache seeded = CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build(); + seeded.put("10.0.0.1", ipMock); + injectCache(seeded); + + Assert.assertTrue(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + InOrder inOrder = Mockito.inOrder(ipMock, globalMock); + inOrder.verify(ipMock).acquire(); + inOrder.verify(globalMock).acquire(); + } + + /** + * If the IP limiter cannot be created (cache loader throws), acquire() + * returns false without consuming a global token — same fail-closed + * behaviour as tryAcquire(). + */ + @Test + public void testAcquireDoesNotConsumeGlobalWhenIpLoaderFails() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + // RateLimiter.create(-1.0) throws IllegalArgumentException, so the + // cache loader fails and loadIpLimiter() returns null. + injectIpQps(-1.0); + injectCache(CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build()); + + Assert.assertFalse(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + Mockito.verify(globalMock, never()).acquire(); + } + + /** + * acquirePermit dispatches based on rate.limiter.apiNonBlocking: + * switch on → only tryAcquire runs; switch off → only acquire runs. + * These tests pin that contract on the static dispatcher; the matching + * default-method contract for IRateLimiter is covered in AdaptorTest. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(true); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.acquire(any()), never()); + } + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(false); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.acquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.acquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + } + } + + private static void injectRateLimiter(RateLimiter rl) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("rateLimiter"); + f.setAccessible(true); + f.set(null, rl); + } + + private static void injectCache(Cache cache) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("cache"); + f.setAccessible(true); + f.set(null, cache); + } + + private static void injectIpQps(double qps) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("IP_QPS"); + f.setAccessible(true); + f.set(null, qps); + } + @AfterClass public static void destroy() { Args.clearParam(); diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java index 6cf02a25050..bbc365f3e0b 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java @@ -95,13 +95,13 @@ public void setUp() throws Exception { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); verify(perEndpoint, never()).release(); } } @@ -112,13 +112,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -129,11 +129,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { @Test public void testGlobalRejectedReleasesPreemptiblePermit() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); interceptor.interceptCall(call, headers, next); @@ -153,12 +153,12 @@ public void testGlobalRejectedReleasesPreemptiblePermit() { @Test public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); when(next.startCall(any(), any())).thenThrow(new RuntimeException("handler crash")); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); @@ -176,14 +176,14 @@ public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception @Test public void testListenerReleasesPermitOnComplete() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onComplete(); @@ -199,14 +199,14 @@ public void testListenerReleasesPermitOnComplete() throws Exception { @Test public void testListenerReleasesPermitOnCancel() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onCancel(); @@ -225,11 +225,11 @@ public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } } diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java index 69a6c688200..5ab85a42bbf 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java @@ -4,12 +4,18 @@ import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ReflectUtils; +import org.tron.core.config.args.Args; +import org.tron.core.services.ratelimiter.RuntimeData; import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; import org.tron.core.services.ratelimiter.strategy.GlobalPreemptibleStrategy; import org.tron.core.services.ratelimiter.strategy.IPQpsStrategy; @@ -17,6 +23,61 @@ public class AdaptorTest { + @Before + public void setUp() { + Args.setParam(new String[0], TestConstants.TEST_CONF); + } + + @AfterClass + public static void tearDown() { + Args.clearParam(); + } + + /** + * IRateLimiter.acquirePermit is a default method that dispatches based on + * rate.limiter.apiNonBlocking. The two cases below pin that contract: with + * the switch on, only tryAcquire is invoked; with the switch off, only + * acquire is invoked. Breaking either direction is a behavioural regression. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(true); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(1, limiter.tryAcquireCount); + Assert.assertEquals(0, limiter.acquireCount); + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(false); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(0, limiter.tryAcquireCount); + Assert.assertEquals(1, limiter.acquireCount); + } + + private static final class CountingRateLimiter implements IRateLimiter { + int tryAcquireCount; + int acquireCount; + + @Override + public boolean tryAcquire(RuntimeData data) { + tryAcquireCount++; + return true; + } + + @Override + public boolean acquire(RuntimeData data) { + acquireCount++; + return true; + } + } + @Test public void testStrategy() { String paramString1 = "qps=5 notExist=6"; diff --git a/framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java b/framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java new file mode 100644 index 00000000000..40a3100c669 --- /dev/null +++ b/framework/src/test/java/org/tron/core/zen/note/BurnCipherTest.java @@ -0,0 +1,300 @@ +package org.tron.core.zen.note; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.utils.ByteUtil; +import org.tron.core.exception.ZksnarkException; +import org.tron.core.zen.note.NoteEncryption.Encryption; + +public class BurnCipherTest { + + private static final byte[] OVK = buildTestBytes(32, 1); + private static final byte[] NF = buildTestBytes(32, 7); + private static final byte[] ADDR_21 = buildAddr21((byte) 0x41); + + private static byte[] buildTestBytes(int len, int seed) { + byte[] data = new byte[len]; + for (int i = 0; i < len; i++) { + data[i] = (byte) (i * 3 + seed); + } + return data; + } + + private static byte[] buildAddr21(byte prefix) { + byte[] addr = new byte[21]; + addr[0] = prefix; + for (int i = 1; i < 21; i++) { + addr[i] = (byte) (i * 2); + } + return addr; + } + + private static byte[] amount32(BigInteger amount) { + return ByteUtil.bigIntegerToBytes(amount, 32); + } + + private static byte[] extractCipher(byte[] record) { + return Arrays.copyOf(record, Encryption.BURN_CIPHER_LEN); + } + + private static byte[] extractNonce(byte[] record) { + return Arrays.copyOfRange(record, + Encryption.BURN_NONCE_OFFSET, + Encryption.BURN_NONCE_OFFSET + Encryption.BURN_NONCE_LEN); + } + + private static byte[] extractReserved(byte[] record) { + return Arrays.copyOfRange(record, + Encryption.BURN_RESERVED_OFFSET, + Encryption.BURN_RESERVED_OFFSET + Encryption.BURN_RESERVED_LEN); + } + + // ---------- constants ---------- + + @Test + public void testBurnCipherSize() { + Assert.assertEquals(80, Encryption.BURN_CIPHER_LEN); + Assert.assertEquals(12, Encryption.BURN_NONCE_LEN); + Assert.assertEquals(4, Encryption.BURN_RESERVED_LEN); + Assert.assertEquals(96, Encryption.BURN_CIPHER_RECORD_SIZE); + } + + // ---------- encrypt ---------- + + @Test + public void testEncryptProduces96ByteRecord() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + Optional recordOpt = Encryption.encryptBurnMessageByOvk( + OVK, amount, ADDR_21, NF); + Assert.assertTrue(recordOpt.isPresent()); + Assert.assertEquals(Encryption.BURN_CIPHER_RECORD_SIZE, recordOpt.get().length); + } + + @Test + public void testRecordReservedBytesCarryV2Marker() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + Assert.assertArrayEquals(new byte[]{0, 0, 0, 1}, extractReserved(record)); + } + + @Test + public void testNonceEmbeddedInRecord() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] nonce = extractNonce(record); + boolean allZero = true; + for (byte b : nonce) { + if (b != 0) { + allZero = false; + break; + } + } + Assert.assertFalse(allZero); + } + + @Test + public void testNonceDeterminism() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + byte[] record1 = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] record2 = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + Assert.assertArrayEquals(record1, record2); + } + + @Test + public void testDifferentNfProducesDifferentRecord() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + byte[] nf2 = new byte[32]; + nf2[0] = (byte) 0xFF; + + byte[] record1 = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] record2 = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, nf2).get(); + Assert.assertFalse(Arrays.equals(record1, record2)); + } + + @Test + public void testDifferentAmountProducesDifferentNonce() throws ZksnarkException { + byte[] record1 = Encryption.encryptBurnMessageByOvk( + OVK, BigInteger.valueOf(1000000), ADDR_21, NF).get(); + byte[] record2 = Encryption.encryptBurnMessageByOvk( + OVK, BigInteger.valueOf(2000000), ADDR_21, NF).get(); + Assert.assertFalse(Arrays.equals(extractNonce(record1), extractNonce(record2))); + } + + @Test + public void testDifferentAddrProducesDifferentNonce() throws ZksnarkException { + byte[] addr2 = ADDR_21.clone(); + addr2[5] ^= (byte) 0xFF; + BigInteger amount = BigInteger.valueOf(1000000); + byte[] record1 = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] record2 = Encryption.encryptBurnMessageByOvk(OVK, amount, addr2, NF).get(); + Assert.assertFalse(Arrays.equals(extractNonce(record1), extractNonce(record2))); + } + + // ---------- encrypt input validation ---------- + + @Test(expected = ZksnarkException.class) + public void testEncryptRejectsNullNf() throws ZksnarkException { + Encryption.encryptBurnMessageByOvk(OVK, BigInteger.ONE, ADDR_21, null); + } + + @Test(expected = ZksnarkException.class) + public void testEncryptRejectsShortOvk() throws ZksnarkException { + Encryption.encryptBurnMessageByOvk(new byte[16], BigInteger.ONE, ADDR_21, NF); + } + + @Test(expected = ZksnarkException.class) + public void testEncryptRejectsBadAddrLength() throws ZksnarkException { + Encryption.encryptBurnMessageByOvk(OVK, BigInteger.ONE, new byte[20], NF); + } + + // ---------- decrypt round-trip ---------- + + @Test + public void testEncryptDecryptRoundTrip() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(1000000); + + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + + Optional plainOpt = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, extractReserved(record), NF, amount32(amount), ADDR_21); + Assert.assertTrue(plainOpt.isPresent()); + byte[] plaintext = plainOpt.get(); + + byte[] decryptedAmount = new byte[32]; + System.arraycopy(plaintext, 0, decryptedAmount, 0, 32); + Assert.assertEquals(amount, ByteUtil.bytesToBigInteger(decryptedAmount)); + + byte[] decryptedAddr = new byte[21]; + System.arraycopy(plaintext, 32, decryptedAddr, 0, 21); + Assert.assertArrayEquals(ADDR_21, decryptedAddr); + } + + @Test + public void testDecryptWithWrongNfFails() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + + byte[] wrongNf = new byte[32]; + wrongNf[0] = (byte) 0xFF; + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, extractReserved(record), wrongNf, amount32(amount), ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptWithWrongAmountFails() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, extractReserved(record), NF, + amount32(BigInteger.valueOf(500001)), ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptWithWrongAddrFails() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + byte[] wrongAddr = ADDR_21.clone(); + wrongAddr[10] ^= (byte) 0xFF; + + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, extractReserved(record), NF, amount32(amount), wrongAddr); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptWithNullNfFailsForV2() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, extractReserved(record), null, amount32(amount), ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptWithWrongNfLengthFailsForV2() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + byte[] reserved = extractReserved(record); + byte[] amt = amount32(amount); + + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, reserved, new byte[31], amt, ADDR_21).isPresent()); + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, reserved, new byte[33], amt, ADDR_21).isPresent()); + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, reserved, new byte[0], amt, ADDR_21).isPresent()); + } + + @Test + public void testDecryptWithTamperedNonceFails() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + + byte[] tamperedNonce = new byte[Encryption.BURN_NONCE_LEN]; + tamperedNonce[0] = (byte) 0xDE; + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, tamperedNonce, extractReserved(record), NF, amount32(amount), ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptWithUnknownReservedMarkerFails() throws ZksnarkException { + BigInteger amount = BigInteger.valueOf(500000); + byte[] record = Encryption.encryptBurnMessageByOvk(OVK, amount, ADDR_21, NF).get(); + byte[] cipher = extractCipher(record); + byte[] nonce = extractNonce(record); + byte[] badReserved = new byte[]{0, 0, 0, 2}; + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, cipher, nonce, badReserved, NF, amount32(amount), ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + // ---------- decrypt input validation ---------- + + @Test(expected = ZksnarkException.class) + public void testDecryptRejectsNullOvk() throws ZksnarkException { + Encryption.decryptBurnMessageByOvk(null, new byte[80], new byte[12], new byte[4], NF, + new byte[32], ADDR_21); + } + + @Test + public void testDecryptRejectsBadCipherLength() throws ZksnarkException { + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, new byte[64], new byte[12], new byte[4], NF, new byte[32], ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptRejectsNullNonce() throws ZksnarkException { + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, new byte[80], null, new byte[4], NF, new byte[32], ADDR_21); + Assert.assertFalse(result.isPresent()); + } + + @Test + public void testDecryptRejectsNullReserved() throws ZksnarkException { + Optional result = Encryption.decryptBurnMessageByOvk( + OVK, new byte[80], new byte[12], null, NF, new byte[32], ADDR_21); + Assert.assertFalse(result.isPresent()); + } +} diff --git a/framework/src/test/java/org/tron/core/zksnark/LibrustzcashTest.java b/framework/src/test/java/org/tron/core/zksnark/LibrustzcashTest.java index 5d403b54f90..b471aeb2e42 100644 --- a/framework/src/test/java/org/tron/core/zksnark/LibrustzcashTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/LibrustzcashTest.java @@ -29,6 +29,7 @@ import org.junit.Ignore; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.zksnark.IncrementalMerkleTreeContainer; @@ -69,7 +70,6 @@ @Slf4j public class LibrustzcashTest extends BaseTest { private static final String dbDirectory = "db_Librustzcash_test"; - private static final String indexDirectory = "index_Librustzcash_test"; @Resource private Wallet wallet; @@ -79,10 +79,9 @@ public static void init() { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); Args.getInstance().setAllowShieldedTransactionApi(true); ZksnarkInitService.librustzcashInitZksnarkParams(); diff --git a/framework/src/test/java/org/tron/core/zksnark/MerkleTreeTest.java b/framework/src/test/java/org/tron/core/zksnark/MerkleTreeTest.java index e21ba8010b5..cf50dc87fa6 100644 --- a/framework/src/test/java/org/tron/core/zksnark/MerkleTreeTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/MerkleTreeTest.java @@ -10,6 +10,7 @@ import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.zksnark.IncrementalMerkleTreeContainer; @@ -27,7 +28,6 @@ public class MerkleTreeTest extends BaseTest { public static final long totalBalance = 1000_0000_000_000L; private static final String dbDirectory = "db_ShieldedTransaction_test"; - private static final String indexDirectory = "index_ShieldedTransaction_test"; private static boolean init; static { @@ -35,10 +35,9 @@ public class MerkleTreeTest extends BaseTest { new String[]{ "--output-directory", dbPath(), "--storage-db-directory", dbDirectory, - "--storage-index-directory", indexDirectory, "--debug" }, - "config-test-mainnet.conf" + TestConstants.TEST_CONF ); } diff --git a/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java b/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java index 3c3fb14b2b1..d94f66bde7f 100644 --- a/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java @@ -1,14 +1,21 @@ package org.tron.core.zksnark; import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import java.math.BigInteger; import java.util.Optional; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.tron.api.GrpcAPI; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.ByteUtil; +import org.tron.common.zksnark.JLibsodium; +import org.tron.common.zksnark.JLibsodiumParam.Chacha20Poly1305IetfEncryptParams; import org.tron.core.Wallet; import org.tron.core.capsule.AssetIssueCapsule; import org.tron.core.config.args.Args; @@ -17,7 +24,9 @@ import org.tron.core.zen.note.NoteEncryption.Encryption; import org.tron.core.zen.note.NoteEncryption.Encryption.OutCiphertext; import org.tron.core.zen.note.OutgoingPlaintext; +import org.tron.protos.Protocol.TransactionInfo; import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; +import org.tron.protos.contract.ShieldContract; @Slf4j public class NoteEncDecryTest extends BaseTest { @@ -39,7 +48,7 @@ public class NoteEncDecryTest extends BaseTest { private Wallet wallet; static { - Args.setParam(new String[]{"--output-directory", dbPath()}, "config-localtest.conf"); + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.SHIELD_CONF); FROM_ADDRESS = Wallet.getAddressPreFixString() + "a7d8a35b260395c14aa456297662092ba3b76fc0"; } @@ -193,4 +202,314 @@ public void testDecryptEncWithEpk() throws ZksnarkException { Assert.assertArrayEquals(rcm, result2.getRcm()); Assert.assertEquals(4000, result2.getValue()); } + + @Test + public void testBurnMessageOvkLegacyZeroNonce() throws ZksnarkException { + byte[] ovk = new byte[]{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}; + byte[] toAddress = new byte[21]; + toAddress[0] = Wallet.getAddressPreFixByte(); + toAddress[20] = 0x42; + BigInteger amount = BigInteger.valueOf(99L); + + byte[] plaintext = new byte[64]; + byte[] amountArr = ByteUtil.bigIntegerToBytes(amount, 32); + System.arraycopy(amountArr, 0, plaintext, 0, 32); + System.arraycopy(toAddress, 0, plaintext, 32, 21); + byte[] zeroNonce = new byte[12]; + byte[] v1Cipher = new byte[Encryption.BURN_CIPHER_LEN]; + int rc = JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt( + new Chacha20Poly1305IetfEncryptParams( + v1Cipher, null, plaintext, 64, null, 0, null, zeroNonce, ovk)); + Assert.assertEquals(0, rc); + + Optional p1 = Encryption.decryptBurnMessageByOvk( + ovk, v1Cipher, new byte[12], new byte[4], null, null, null); + Assert.assertTrue(p1.isPresent()); + Assert.assertArrayEquals(plaintext, p1.get()); + + byte[] wrongNonce = new byte[12]; + wrongNonce[0] = 1; + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + ovk, v1Cipher, wrongNonce, new byte[4], null, null, null).isPresent()); + + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + ovk, v1Cipher, new byte[11], new byte[4], null, null, null).isPresent()); + Assert.assertFalse(Encryption.decryptBurnMessageByOvk( + ovk, v1Cipher, null, new byte[4], null, null, null).isPresent()); + } + + @Test + public void testGetTriggerInputBurnV2Accepted() throws Exception { + byte[] nf = new byte[32]; + for (int i = 0; i < nf.length; i++) { + nf[i] = (byte) (i + 1); + } + byte[] burnRecord = buildV2BurnRecord(nf); + GrpcAPI.ShieldedTRC20Parameters trc20Params = buildBurnTrc20Params(burnRecord, nf); + GrpcAPI.ShieldedTRC20TriggerContractParameters req = buildBurnTriggerRequest( + trc20Params, BigInteger.ONE); + GrpcAPI.BytesMessage out = wallet.getTriggerInputForShieldedTRC20Contract(req); + Assert.assertNotNull(out); + } + + @Test + public void testGetTriggerInputBurnLegacy96ByteRecordRejected() throws Exception { + byte[] allZeroRecord = new byte[Encryption.BURN_CIPHER_RECORD_SIZE]; + byte[] nf = new byte[32]; + GrpcAPI.ShieldedTRC20Parameters trc20Params = buildBurnTrc20Params(allZeroRecord, nf); + GrpcAPI.ShieldedTRC20TriggerContractParameters req = buildBurnTriggerRequest( + trc20Params, BigInteger.ONE); + try { + wallet.getTriggerInputForShieldedTRC20Contract(req); + Assert.fail("expected ZksnarkException for legacy 96-byte burn record"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage(), e.getMessage().contains("v2")); + } + } + + @Test + public void testGetTriggerInputBurnUnknownReservedRejected() throws Exception { + byte[] nf = new byte[32]; + nf[0] = 0x5A; + byte[] record = buildV2BurnRecord(nf); + // mutate reserved to an unknown marker (0x00000002). + record[Encryption.BURN_RESERVED_OFFSET + Encryption.BURN_RESERVED_LEN - 1] = 2; + GrpcAPI.ShieldedTRC20Parameters trc20Params = buildBurnTrc20Params(record, nf); + GrpcAPI.ShieldedTRC20TriggerContractParameters req = buildBurnTriggerRequest( + trc20Params, BigInteger.ONE); + try { + wallet.getTriggerInputForShieldedTRC20Contract(req); + Assert.fail("expected ZksnarkException for unknown reserved marker"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage(), e.getMessage().contains("v2")); + } + } + + @Test + public void testGetTriggerInputBurnNonceMismatchRejected() throws Exception { + byte[] nf = new byte[32]; + nf[0] = 0x11; + byte[] record = buildV2BurnRecord(nf); + // flip one nonce byte so it no longer matches deriveBurnNonce(nf, amount, addr). + record[Encryption.BURN_NONCE_OFFSET] ^= (byte) 0xFF; + GrpcAPI.ShieldedTRC20Parameters trc20Params = buildBurnTrc20Params(record, nf); + GrpcAPI.ShieldedTRC20TriggerContractParameters req = buildBurnTriggerRequest( + trc20Params, BigInteger.ONE); + try { + wallet.getTriggerInputForShieldedTRC20Contract(req); + Assert.fail("expected ZksnarkException for mismatched nf-bound nonce"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage(), e.getMessage().contains("nonce")); + } + } + + @Test + public void testGetTriggerInputBurn80ByteCipherRejected() throws Exception { + byte[] legacyCipher = new byte[Encryption.BURN_CIPHER_LEN]; + byte[] nf = new byte[32]; + GrpcAPI.ShieldedTRC20Parameters trc20Params = buildBurnTrc20Params(legacyCipher, nf); + GrpcAPI.ShieldedTRC20TriggerContractParameters req = buildBurnTriggerRequest( + trc20Params, BigInteger.ONE); + try { + wallet.getTriggerInputForShieldedTRC20Contract(req); + Assert.fail("expected ZksnarkException for 80-byte burn cipher"); + } catch (ZksnarkException e) { + Assert.assertTrue(e.getMessage().contains("deprecated")); + } + } + + private static byte[] buildV2BurnRecord(byte[] nf) { + byte[] record = new byte[Encryption.BURN_CIPHER_RECORD_SIZE]; + // cipher(0..80) left as zeros — getTriggerInputForShieldedTRC20Contract only + // checks reserved marker and nonce binding to (nf, amount, addr), not cipher decryptability. + byte[] amount32 = ByteUtil.bigIntegerToBytes(BigInteger.ONE, 32); + byte[] addr21 = new byte[21]; + addr21[0] = Wallet.getAddressPreFixByte(); + byte[] nonce = Encryption.deriveBurnNonce(nf, amount32, addr21); + System.arraycopy(nonce, 0, record, Encryption.BURN_NONCE_OFFSET, Encryption.BURN_NONCE_LEN); + byte[] marker = Encryption.getBurnRecordV2Marker(); + System.arraycopy(marker, 0, record, Encryption.BURN_RESERVED_OFFSET, + Encryption.BURN_RESERVED_LEN); + return record; + } + + @Test + public void testGetNoteTxFromLogListByOvkBurnTooShort() throws Exception { + Wallet w = new Wallet(); + byte[] ovk = new byte[32]; + byte[] logData = new byte[64 + Encryption.BURN_CIPHER_RECORD_SIZE - 1]; + TransactionInfo.Log log = TransactionInfo.Log.newBuilder() + .setData(ByteString.copyFrom(logData)).build(); + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder builder = + GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(); + + Method m = Wallet.class.getDeclaredMethod("getNoteTxFromLogListByOvk", + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder.class, + TransactionInfo.Log.class, byte[].class, int.class, byte[].class); + m.setAccessible(true); + Object result = m.invoke(w, builder, log, ovk, 4, null); + Assert.assertFalse(((Optional) result).isPresent()); + } + + @Test + public void testGetNoteTxFromLogListByOvkBurnRoundTrip() throws Exception { + Wallet w = new Wallet(); + byte[] ovk = new byte[32]; + for (int i = 0; i < 32; i++) { + ovk[i] = (byte) (i + 1); + } + BigInteger amount = BigInteger.valueOf(1000L); + byte[] toAddress = new byte[21]; + toAddress[0] = Wallet.getAddressPreFixByte(); + toAddress[20] = 0x42; + byte[] nf = new byte[32]; + nf[0] = (byte) 0xAB; + + Optional recordOpt = Encryption.encryptBurnMessageByOvk( + ovk, amount, toAddress, nf); + Assert.assertTrue(recordOpt.isPresent()); + byte[] record = recordOpt.get(); + + byte[] logData = new byte[64 + Encryption.BURN_CIPHER_RECORD_SIZE]; + System.arraycopy(toAddress, 1, logData, 12, 20); + byte[] valBytes = ByteUtil.bigIntegerToBytes(amount, 32); + System.arraycopy(valBytes, 0, logData, 32, 32); + System.arraycopy(record, 0, logData, 64, Encryption.BURN_CIPHER_RECORD_SIZE); + + TransactionInfo.Log log = TransactionInfo.Log.newBuilder() + .setData(ByteString.copyFrom(logData)).build(); + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder builder = + GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(); + + Method m = Wallet.class.getDeclaredMethod("getNoteTxFromLogListByOvk", + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder.class, + TransactionInfo.Log.class, byte[].class, int.class, byte[].class); + m.setAccessible(true); + Object result = m.invoke(w, builder, log, ovk, 4, nf); + Assert.assertTrue(((Optional) result).isPresent()); + } + + @Test + public void testGetNoteTxFromLogListByOvkBurnMissingNfRejected() throws Exception { + Wallet w = new Wallet(); + byte[] ovk = new byte[32]; + for (int i = 0; i < 32; i++) { + ovk[i] = (byte) (i + 1); + } + BigInteger amount = BigInteger.valueOf(1000L); + byte[] toAddress = new byte[21]; + toAddress[0] = Wallet.getAddressPreFixByte(); + toAddress[20] = 0x42; + byte[] nf = new byte[32]; + nf[0] = (byte) 0xAB; + + Optional recordOpt = Encryption.encryptBurnMessageByOvk( + ovk, amount, toAddress, nf); + Assert.assertTrue(recordOpt.isPresent()); + byte[] record = recordOpt.get(); + + byte[] logData = new byte[64 + Encryption.BURN_CIPHER_RECORD_SIZE]; + System.arraycopy(toAddress, 1, logData, 12, 20); + byte[] valBytes = ByteUtil.bigIntegerToBytes(amount, 32); + System.arraycopy(valBytes, 0, logData, 32, 32); + System.arraycopy(record, 0, logData, 64, Encryption.BURN_CIPHER_RECORD_SIZE); + + TransactionInfo.Log log = TransactionInfo.Log.newBuilder() + .setData(ByteString.copyFrom(logData)).build(); + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder builder = + GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(); + + Method m = Wallet.class.getDeclaredMethod("getNoteTxFromLogListByOvk", + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder.class, + TransactionInfo.Log.class, byte[].class, int.class, byte[].class); + m.setAccessible(true); + Object result = m.invoke(w, builder, log, ovk, 4, null); + Assert.assertFalse(((Optional) result).isPresent()); + } + + @Test + public void testGetNoteTxFromLogListByOvkTwoBurnsCursorPairing() throws Exception { + Wallet w = new Wallet(); + byte[] ovk = new byte[32]; + for (int i = 0; i < 32; i++) { + ovk[i] = (byte) (i + 1); + } + byte[] toAddress = new byte[21]; + toAddress[0] = Wallet.getAddressPreFixByte(); + toAddress[20] = 0x42; + + byte[] nf1 = new byte[32]; + nf1[0] = (byte) 0xAA; + byte[] nf2 = new byte[32]; + nf2[0] = (byte) 0xBB; + BigInteger amount1 = BigInteger.valueOf(1000L); + BigInteger amount2 = BigInteger.valueOf(2000L); + + TransactionInfo.Log log1 = buildBurnLog(ovk, amount1, toAddress, nf1); + TransactionInfo.Log log2 = buildBurnLog(ovk, amount2, toAddress, nf2); + + Method m = Wallet.class.getDeclaredMethod("getNoteTxFromLogListByOvk", + GrpcAPI.DecryptNotesTRC20.NoteTx.Builder.class, + TransactionInfo.Log.class, byte[].class, int.class, byte[].class); + m.setAccessible(true); + + // correct cursor pairing: each log decrypted with its own nf + Optional r1 = (Optional) m.invoke( + w, GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(), log1, ovk, 4, nf1); + Optional r2 = (Optional) m.invoke( + w, GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(), log2, ovk, 4, nf2); + Assert.assertTrue("burn1 should decrypt with nf1", r1.isPresent()); + Assert.assertTrue("burn2 should decrypt with nf2", r2.isPresent()); + GrpcAPI.DecryptNotesTRC20.NoteTx tx1 = (GrpcAPI.DecryptNotesTRC20.NoteTx) r1.get(); + GrpcAPI.DecryptNotesTRC20.NoteTx tx2 = (GrpcAPI.DecryptNotesTRC20.NoteTx) r2.get(); + Assert.assertEquals(amount1.toString(10), tx1.getToAmount()); + Assert.assertEquals(amount2.toString(10), tx2.getToAmount()); + + // mis-paired cursor: nonce-from-log mismatches sha3(domain||nf||amount||addr), strict rejects + Optional bad1 = (Optional) m.invoke( + w, GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(), log1, ovk, 4, nf2); + Optional bad2 = (Optional) m.invoke( + w, GrpcAPI.DecryptNotesTRC20.NoteTx.newBuilder(), log2, ovk, 4, nf1); + Assert.assertFalse("burn1 must not decrypt under nf2", bad1.isPresent()); + Assert.assertFalse("burn2 must not decrypt under nf1", bad2.isPresent()); + } + + private TransactionInfo.Log buildBurnLog(byte[] ovk, BigInteger amount, byte[] toAddress, + byte[] nf) throws ZksnarkException { + Optional recordOpt = Encryption.encryptBurnMessageByOvk(ovk, amount, toAddress, nf); + Assert.assertTrue(recordOpt.isPresent()); + byte[] record = recordOpt.get(); + byte[] logData = new byte[64 + Encryption.BURN_CIPHER_RECORD_SIZE]; + System.arraycopy(toAddress, 1, logData, 12, 20); + byte[] valBytes = ByteUtil.bigIntegerToBytes(amount, 32); + System.arraycopy(valBytes, 0, logData, 32, 32); + System.arraycopy(record, 0, logData, 64, Encryption.BURN_CIPHER_RECORD_SIZE); + return TransactionInfo.Log.newBuilder() + .setData(ByteString.copyFrom(logData)).build(); + } + + private GrpcAPI.ShieldedTRC20Parameters buildBurnTrc20Params(byte[] cipher, byte[] nf) { + ShieldContract.SpendDescription spend = ShieldContract.SpendDescription.newBuilder() + .setNullifier(ByteString.copyFrom(nf)) + .build(); + return GrpcAPI.ShieldedTRC20Parameters.newBuilder() + .setParameterType("burn") + .setTriggerContractInput(ByteArray.toHexString(cipher)) + .addSpendDescription(spend) + .build(); + } + + private GrpcAPI.ShieldedTRC20TriggerContractParameters buildBurnTriggerRequest( + GrpcAPI.ShieldedTRC20Parameters trc20Params, BigInteger value) { + byte[] toAddress = new byte[21]; + toAddress[0] = Wallet.getAddressPreFixByte(); + return GrpcAPI.ShieldedTRC20TriggerContractParameters.newBuilder() + .setShieldedTRC20Parameters(trc20Params) + .addSpendAuthoritySignature(GrpcAPI.BytesMessage.getDefaultInstance()) + .setAmount(value.toString()) + .setTransparentToAddress(ByteString.copyFrom(toAddress)) + .build(); + } } diff --git a/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java b/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java index 8693bf0716d..08de83ca8bf 100644 --- a/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.tron.api.GrpcAPI; import org.tron.common.BaseTest; +import org.tron.common.TestConstants; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; @@ -111,7 +112,7 @@ public class SendCoinShieldTest extends BaseTest { private static boolean init; static { - Args.setParam(new String[]{"--output-directory", dbPath()}, "config-test-mainnet.conf"); + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); Args.getInstance().setZenTokenId(String.valueOf(tokenId)); PUBLIC_ADDRESS_ONE = Wallet.getAddressPreFixString() + "a7d8a35b260395c14aa456297662092ba3b76fc0"; 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 7143cef43e2..5854b731e97 100755 --- a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java @@ -1,6 +1,6 @@ package org.tron.core.zksnark; -import static org.tron.common.TestConstants.LOCAL_CONF; +import static org.tron.common.TestConstants.SHIELD_CONF; import static org.tron.common.utils.PublicMethod.getHexAddressByPrivateKey; import static org.tron.common.utils.PublicMethod.getRandomPrivateKey; @@ -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; @@ -34,7 +35,6 @@ import org.tron.common.crypto.ECKey; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; -import org.tron.common.utils.PublicMethod; import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.client.utils.TransactionUtils; import org.tron.common.zksnark.IncrementalMerkleTreeContainer; @@ -46,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; @@ -141,15 +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"}, - LOCAL_CONF); + Args.setParam(new String[] {"--output-directory", dbPath(), "-w"}, SHIELD_CONF); ADDRESS_ONE_PRIVATE_KEY = getRandomPrivateKey(); FROM_ADDRESS = getHexAddressByPrivateKey(ADDRESS_ONE_PRIVATE_KEY); } @@ -333,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) { @@ -435,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())}; } @@ -2404,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/json/JsonTest.java b/framework/src/test/java/org/tron/json/JsonTest.java index 2a6d73931be..f430188611d 100644 --- a/framework/src/test/java/org/tron/json/JsonTest.java +++ b/framework/src/test/java/org/tron/json/JsonTest.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertTrue; import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.node.NullNode; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; @@ -16,6 +17,7 @@ import java.util.List; import java.util.Locale; import org.junit.Test; +import org.tron.core.Constant; /** * Tests for Jackson {@code JsonReadFeature} compatibility with Fastjson 1.x. @@ -157,6 +159,22 @@ public void testToJSONString() { assertEquals("\"hi\"", JSON.toJSONString("hi", true)); } + @Test + public void testExplicitNullNodeSerializationIsPreserved() { + JSONObject parsedNull = JSON.parseObject("{\"a\":null,\"b\":1}"); + assertTrue(parsedNull.containsKey("a")); + assertNull(parsedNull.get("a")); + assertEquals("{\"a\":null,\"b\":1}", parsedNull.toJSONString()); + + JSONObject explicitNull = new JSONObject().put("a", NullNode.getInstance()).put("b", 1); + assertTrue(explicitNull.containsKey("a")); + assertEquals("{\"a\":null,\"b\":1}", explicitNull.toJSONString()); + + explicitNull.put("a", (Object) null); + assertFalse(explicitNull.containsKey("a")); + assertEquals("{\"b\":1}", explicitNull.toJSONString()); + } + @Test public void testJsonObjectGetters() { JSONObject o = JSON.parseObject( @@ -369,8 +387,8 @@ public void testTypeUtilsCoercion() { @Test public void testJsonMapperHasConfiguredConstraints() { StreamReadConstraints sr = JSON.MAPPER.getFactory().streamReadConstraints(); - assertEquals(100, sr.getMaxNestingDepth()); - assertEquals(100_000L, sr.getMaxTokenCount()); + assertEquals(Constant.MAX_NESTING_DEPTH, sr.getMaxNestingDepth()); + assertEquals((long) Constant.MAX_TOKEN_COUNT, sr.getMaxTokenCount()); } @Test diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java index a02eb22364e..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; @@ -75,7 +76,7 @@ private void setFlag(boolean value) throws Exception { f.set(solidityNode, value); } - // ── existing tests ──────────────────────────────────────────────────────────── + // ── gRPC / HTTP service integration ────────────────────────────────────────── @Test public void testSolidityGrpcCall() { @@ -115,7 +116,7 @@ public void testSolidityNodeHttpApiService() { Assert.assertTrue(true); } - // ── new tests ───────────────────────────────────────────────────────────────── + // ── lifecycle ───────────────────────────────────────────────────────────────── /** * @PostConstruct init() must create both executor services before run() is called. @@ -146,234 +147,353 @@ public void testOnApplicationEventSetsFlagFalse() throws Exception { } /** - * getBlockByNum() must throw RuntimeException (not return null) when - * flag=false, to prevent NullPointerException in blockQueue.put(). + * SolidityCondition must match when --solidity is passed so the bean is + * registered in the Spring context. */ - @Test(timeout = 1000) - public void testGetBlockByNumThrowsWhenClosed() throws Exception { - setFlag(false); - try { - Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); - m.setAccessible(true); - try { - m.invoke(solidityNode, 1L); - Assert.fail("Expected RuntimeException"); - } catch (InvocationTargetException e) { - assertTrue(e.getCause() instanceof RuntimeException); - assertEquals("SolidityNode is closing.", e.getCause().getMessage()); - } - } finally { - setFlag(true); - } + @Test + public void testSolidityConditionMatchesWhenSolidityFlagSet() { + assertTrue(Args.getInstance().isSolidityNode()); + SolidityNode.SolidityCondition condition = new SolidityNode.SolidityCondition(); + assertTrue(condition.matches( + mock(ConditionContext.class), + mock(AnnotatedTypeMetadata.class))); } /** - * getLastSolidityBlockNum() must return 0 (not throw) when flag=false so - * getBlock()'s while(flag) loop exits quietly without a misleading error log. + * resolveCompatibilityIssueIfUsingFullNodeDatabase() must update the solidified + * block num to match headBlockNum when solidity lags behind. */ - @Test(timeout = 1000) - public void testGetLastSolidityBlockNumReturnsZeroWhenClosed() throws Exception { - setFlag(false); + @Test(timeout = 2000) + public void testResolveCompatibilityIssueWhenSolidityLagsHead() throws Exception { + DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); + Mockito.when(mockStore.getLatestSolidifiedBlockNum()).thenReturn(3L); + ChainBaseManager mockCbm = mock(ChainBaseManager.class); + Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); + Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(10L); + + Field cbmField = getField("chainBaseManager"); + Object orig = cbmField.get(solidityNode); + cbmField.set(solidityNode, mockCbm); try { - Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + Method m = SolidityNode.class.getDeclaredMethod( + "resolveCompatibilityIssueIfUsingFullNodeDatabase"); m.setAccessible(true); - long result = (long) m.invoke(solidityNode); - assertEquals(0L, result); + m.invoke(solidityNode); } finally { - setFlag(true); + cbmField.set(solidityNode, orig); } + Mockito.verify(mockStore).saveLatestSolidifiedBlockNum(10L); } /** - * SolidityCondition must match when --solidity is passed so the bean is - * registered in the Spring context. + * When databaseGrpcClient is non-null at shutdown time, its shutdown() must + * be called to close the gRPC channel. */ @Test - public void testSolidityConditionMatchesWhenSolidityFlagSet() { - assertTrue(Args.getInstance().isSolidityNode()); - SolidityNode.SolidityCondition condition = new SolidityNode.SolidityCondition(); - assertTrue(condition.matches( - mock(ConditionContext.class), - mock(AnnotatedTypeMetadata.class))); - } + public void testShutdownCallsDatabaseClientShutdown() throws Exception { + // Use a standalone instance so we don't destroy the shared Spring executor services. + SolidityNode node = new SolidityNode(); - // ── additional coverage tests ───────────────────────────────────────────────── + DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); + ChainBaseManager mockCbm = mock(ChainBaseManager.class); + Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); + Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(0L); + getField("chainBaseManager").set(node, mockCbm); - /** - * sleep() must return normally without throwing. - */ - @Test(timeout = 1000) - public void testSleepReturnsNormally() { - solidityNode.sleep(1); + Method initM = SolidityNode.class.getDeclaredMethod("init"); + initM.setAccessible(true); + initM.invoke(node); + + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + getField("databaseGrpcClient").set(node, mockClient); + + Method shutdownM = SolidityNode.class.getDeclaredMethod("shutdown"); + shutdownM.setAccessible(true); + shutdownM.invoke(node); + + Mockito.verify(mockClient).shutdown(); } + // ── sleep() ─────────────────────────────────────────────────────────────────── + /** - * sleep() must swallow InterruptedException so callers are not surprised; - * the thread continues after waking. + * sleep() must: + * - return normally without throwing on a plain call, + * - exit early when the thread is interrupted, + * - restore the interrupt flag so callers can observe it immediately. */ @Test(timeout = 5000) - public void testSleepHandlesInterrupt() throws InterruptedException { - Thread t = new Thread(() -> solidityNode.sleep(10_000)); + public void testSleep() throws InterruptedException { + // Normal: returns without throwing. + solidityNode.sleep(1); + + // Interrupt: exits early + restores flag. + boolean[] flagAfterSleep = {false}; + Thread t = new Thread(() -> { + solidityNode.sleep(10_000); + flagAfterSleep[0] = Thread.currentThread().isInterrupted(); + }); t.start(); Thread.sleep(50); t.interrupt(); t.join(2000); - assertFalse("sleep() should have returned after interrupt", t.isAlive()); + assertFalse("sleep() must return after interrupt", t.isAlive()); + assertTrue("sleep() must restore the interrupt flag", flagAfterSleep[0]); } + // ── getBlockByNum() ─────────────────────────────────────────────────────────── + /** - * getBlockByNum() must return the block when the gRPC client returns a block - * whose number matches the requested number. + * getBlockByNum() normal-path and transient-error recovery: + * - happy path: returns the block when the gRPC response number matches, + * - null response: warns and retries on the next iteration, + * - RPC exception: logs, sleeps, and succeeds on the second attempt. */ - @Test(timeout = 2000) - public void testGetBlockByNumReturnsMatchingBlock() throws Exception { - Block expected = blockWithNum(7L); - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getBlock(7L)).thenReturn(expected); - + @Test(timeout = 6000) + public void testGetBlockByNum() throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); Field clientField = getField("databaseGrpcClient"); Object orig = clientField.get(solidityNode); - clientField.set(solidityNode, mockClient); try { - Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); - m.setAccessible(true); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + clientField.set(solidityNode, mockClient); + + // Happy path: matching block returned directly. + Mockito.when(mockClient.getBlock(7L)).thenReturn(blockWithNum(7L)); Block result = (Block) m.invoke(solidityNode, 7L); assertEquals(7L, result.getBlockHeader().getRawData().getNumber()); + + // Null response: warn + retry, succeed on second call. + Mockito.when(mockClient.getBlock(5L)) + .thenReturn(null) + .thenReturn(blockWithNum(5L)); + result = (Block) m.invoke(solidityNode, 5L); + assertEquals(5L, result.getBlockHeader().getRawData().getNumber()); + Mockito.verify(mockClient, Mockito.times(2)).getBlock(5L); + + // RPC exception: log + retry, succeed on second call. + Mockito.when(mockClient.getBlock(8L)) + .thenThrow(new RuntimeException("rpc error")) + .thenReturn(blockWithNum(8L)); + result = (Block) m.invoke(solidityNode, 8L); + assertEquals(8L, result.getBlockHeader().getRawData().getNumber()); } finally { clientField.set(solidityNode, orig); } } /** - * getLastSolidityBlockNum() must return the value obtained from the gRPC - * client when the call succeeds. + * getBlockByNum() shutdown paths: must throw RuntimeException (not return + * null) in two cases so callers can detect closure cleanly: + * - flag=false before the loop starts (immediate exit), + * - wrong block number returned and flag races to false during the retry sleep. */ - @Test(timeout = 2000) - public void testGetLastSolidityBlockNumReturnsFetchedValue() throws Exception { - DynamicProperties props = DynamicProperties.newBuilder() - .setLastSolidityBlockNum(99L).build(); - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getDynamicProperties()).thenReturn(props); + @Test(timeout = 5000) + public void testGetBlockByNumWhenClosed() throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + + // flag=false: while condition exits immediately. + setFlag(false); + try { + try { + m.invoke(solidityNode, 1L); + Assert.fail("Expected RuntimeException"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof RuntimeException); + assertEquals("SolidityNode is closing.", e.getCause().getMessage()); + } + } finally { + setFlag(true); + } + // Wrong block number returned: flag goes false → loop exits → throws. + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(9L)).thenAnswer(inv -> { + setFlag(false); + return blockWithNum(999L); + }); Field clientField = getField("databaseGrpcClient"); Object orig = clientField.get(solidityNode); clientField.set(solidityNode, mockClient); try { - Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); - m.setAccessible(true); - long result = (long) m.invoke(solidityNode); - assertEquals(99L, result); + try { + m.invoke(solidityNode, 9L); + Assert.fail("Expected RuntimeException"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof RuntimeException); + } } finally { + setFlag(true); clientField.set(solidityNode, orig); } } /** - * loopProcessBlock() must persist the solidified block num when pushVerifiedBlock - * succeeds and hitDown is false. + * 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 testLoopProcessBlockSavesBlockNumWhenNotHitDown() throws Exception { - TronNetDelegate mockDelegate = mock(TronNetDelegate.class); - Mockito.when(mockDelegate.isHitDown()).thenReturn(false); - - long origSolidified = chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum(); - Field delegateField = getField("tronNetDelegate"); - Object origDelegate = delegateField.get(solidityNode); - delegateField.set(solidityNode, mockDelegate); + 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 { - invokeLoopProcessBlock(blockWithNum(55L)); - assertEquals(55L, chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum()); + 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 { - chainBaseManager.getDynamicPropertiesStore() - .saveLatestSolidifiedBlockNum(origSolidified); - delegateField.set(solidityNode, origDelegate); + setFlag(true); + clientField.set(solidityNode, origClient); } } + // ── getLastSolidityBlockNum() ───────────────────────────────────────────────── + /** - * loopProcessBlock() must NOT persist the solidified block num when hitDown - * is true, because the block was never pushed to BlockStore. + * getLastSolidityBlockNum() normal-path and retry: + * - happy path: returns the value from getDynamicProperties(), + * - RPC exception: logs, sleeps, and returns the value on the second attempt. */ - @Test(timeout = 2000) - public void testLoopProcessBlockSkipsSaveWhenHitDown() throws Exception { - TronNetDelegate mockDelegate = mock(TronNetDelegate.class); - Mockito.when(mockDelegate.isHitDown()).thenReturn(true); - - long origSolidified = chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum(); - Field delegateField = getField("tronNetDelegate"); - Object origDelegate = delegateField.get(solidityNode); - delegateField.set(solidityNode, mockDelegate); + @Test(timeout = 4000) + public void testGetLastSolidityBlockNum() throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + m.setAccessible(true); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); try { - invokeLoopProcessBlock(blockWithNum(56L)); - assertEquals(origSolidified, chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum()); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + clientField.set(solidityNode, mockClient); + + // Happy path. + Mockito.when(mockClient.getDynamicProperties()) + .thenReturn(DynamicProperties.newBuilder().setLastSolidityBlockNum(99L).build()); + assertEquals(99L, (long) m.invoke(solidityNode)); + + // RPC exception: retry, return value on second attempt. + Mockito.when(mockClient.getDynamicProperties()) + .thenThrow(new RuntimeException("rpc error")) + .thenReturn(DynamicProperties.newBuilder().setLastSolidityBlockNum(50L).build()); + assertEquals(50L, (long) m.invoke(solidityNode)); } finally { - delegateField.set(solidityNode, origDelegate); + clientField.set(solidityNode, orig); } } /** - * resolveCompatibilityIssueIfUsingFullNodeDatabase() must update the solidified - * block num to match headBlockNum when solidity lags behind. + * getLastSolidityBlockNum() shutdown paths: must return 0 without looping in + * two cases: + * - flag=false before the loop starts (while condition fails), + * - exception thrown after flag races to false during the gRPC call. */ - @Test(timeout = 2000) - public void testResolveCompatibilityIssueWhenSolidityLagsHead() throws Exception { - DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); - Mockito.when(mockStore.getLatestSolidifiedBlockNum()).thenReturn(3L); - ChainBaseManager mockCbm = mock(ChainBaseManager.class); - Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); - Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(10L); + @Test(timeout = 3000) + public void testGetLastSolidityBlockNumWhenClosed() throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + m.setAccessible(true); - Field cbmField = getField("chainBaseManager"); - Object orig = cbmField.get(solidityNode); - cbmField.set(solidityNode, mockCbm); + // flag=false: while condition exits immediately, returns 0. + setFlag(false); try { - Method m = SolidityNode.class.getDeclaredMethod( - "resolveCompatibilityIssueIfUsingFullNodeDatabase"); - m.setAccessible(true); - m.invoke(solidityNode); + assertEquals(0L, (long) m.invoke(solidityNode)); } finally { - cbmField.set(solidityNode, orig); + setFlag(true); + } + + // Exception while flag races to false: !flag guard returns 0 with INFO. + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getDynamicProperties()).thenAnswer(inv -> { + setFlag(false); + throw new RuntimeException("channel closed during shutdown"); + }); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + assertEquals(0L, (long) m.invoke(solidityNode)); + } finally { + setFlag(true); + clientField.set(solidityNode, orig); } - Mockito.verify(mockStore).saveLatestSolidifiedBlockNum(10L); } - // ── shutdown / databaseGrpcClient lifecycle ────────────────────────────────── + // ── loopProcessBlock() ──────────────────────────────────────────────────────── /** - * When databaseGrpcClient is non-null at shutdown time, its shutdown() must - * be called to close the gRPC channel. + * loopProcessBlock() behaviour across three scenarios: + * - hitDown=false: solidified block num is persisted after a successful push, + * - hitDown=true: solidified block num is NOT updated (block not in store), + * - push throws on first attempt: retries after sleep and succeeds on second. */ - @Test - public void testShutdownCallsDatabaseClientShutdown() throws Exception { - // Use a standalone instance so we don't destroy the shared Spring executor services. - SolidityNode node = new SolidityNode(); - - DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); - ChainBaseManager mockCbm = mock(ChainBaseManager.class); - Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); - Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(0L); - getField("chainBaseManager").set(node, mockCbm); - - Method initM = SolidityNode.class.getDeclaredMethod("init"); - initM.setAccessible(true); - initM.invoke(node); - - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - getField("databaseGrpcClient").set(node, mockClient); - - Method shutdownM = SolidityNode.class.getDeclaredMethod("shutdown"); - shutdownM.setAccessible(true); - shutdownM.invoke(node); + @Test(timeout = 6000) + public void testLoopProcessBlock() throws Exception { + long origSolidified = chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum(); + Field delegateField = getField("tronNetDelegate"); + Field clientField = getField("databaseGrpcClient"); + Object origDelegate = delegateField.get(solidityNode); + Object origClient = clientField.get(solidityNode); + try { + // hitDown=false: solidified block num must be saved. + TronNetDelegate notHitDown = mock(TronNetDelegate.class); + Mockito.when(notHitDown.isHitDown()).thenReturn(false); + delegateField.set(solidityNode, notHitDown); + invokeLoopProcessBlock(blockWithNum(55L)); + assertEquals(55L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); - Mockito.verify(mockClient).shutdown(); + // hitDown=true: solidified block num must NOT change. + TronNetDelegate hitDown = mock(TronNetDelegate.class); + Mockito.when(hitDown.isHitDown()).thenReturn(true); + delegateField.set(solidityNode, hitDown); + invokeLoopProcessBlock(blockWithNum(56L)); + assertEquals(55L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); // unchanged + + // Exception on first push: sleep, re-fetch, succeed on second push. + TronNetDelegate retryDelegate = mock(TronNetDelegate.class); + Mockito.when(retryDelegate.isHitDown()).thenReturn(false); + Mockito.doThrow(new RuntimeException("push failed")) + .doNothing() + .when(retryDelegate).pushVerifiedBlock(Mockito.any()); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(33L)).thenReturn(blockWithNum(33L)); + delegateField.set(solidityNode, retryDelegate); + clientField.set(solidityNode, mockClient); + invokeLoopProcessBlock(blockWithNum(33L)); + assertEquals(33L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); + } finally { + chainBaseManager.getDynamicPropertiesStore() + .saveLatestSolidifiedBlockNum(origSolidified); + delegateField.set(solidityNode, origDelegate); + clientField.set(solidityNode, origClient); + } } - // ── getBlock() ─────────────────────────────────────────────────────────────── + // ── getBlock() ──────────────────────────────────────────────────────────────── /** * getBlock() must fetch a block via gRPC, place it in blockQueue, then exit @@ -382,24 +502,24 @@ public void testShutdownCallsDatabaseClientShutdown() throws Exception { @Test(timeout = 5000) @SuppressWarnings("unchecked") public void testGetBlockProcessesOneBlock() throws Exception { - long origID = atomicLong("ID").get(); + long origID = atomicLong("ID").get(); long origRemote = atomicLong("remoteBlockNum").get(); atomicLong("ID").set(0L); - atomicLong("remoteBlockNum").set(2L); // blockNum=1 <= 2, no sleep needed + atomicLong("remoteBlockNum").set(2L); DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); Mockito.when(mockClient.getBlock(1L)).thenAnswer(inv -> { - setFlag(false); // stop the loop after this iteration + setFlag(false); return blockWithNum(1L); }); TronNetDelegate mockDelegate = mock(TronNetDelegate.class); Mockito.when(mockDelegate.isHitDown()).thenReturn(false); - Field clientField = getField("databaseGrpcClient"); + Field clientField = getField("databaseGrpcClient"); Field delegateField = getField("tronNetDelegate"); - Object origClient = clientField.get(solidityNode); + Object origClient = clientField.get(solidityNode); Object origDelegate = delegateField.get(solidityNode); clientField.set(solidityNode, mockClient); delegateField.set(solidityNode, mockDelegate); @@ -412,7 +532,87 @@ public void testGetBlockProcessesOneBlock() throws Exception { m.invoke(solidityNode); assertEquals(1, queue.size()); - assertEquals(1L, queue.peek().getBlockHeader().getRawData().getNumber()); + Block peeked = queue.peek(); + Assert.assertNotNull("blockQueue must contain the fetched block", peeked); + assertEquals(1L, peeked.getBlockHeader().getRawData().getNumber()); + } finally { + setFlag(true); + queue.clear(); + atomicLong("ID").set(origID); + atomicLong("remoteBlockNum").set(origRemote); + clientField.set(solidityNode, origClient); + delegateField.set(solidityNode, origDelegate); + } + } + + /** + * getBlock() shutdown paths: + * - interrupted in blockQueue.put() by shutdownNow(): must exit cleanly with + * INFO (root cause of the original "reason: null" ERROR bug), + * - exception thrown while flag is already false: must exit cleanly with INFO + * instead of logging ERROR and retrying. + */ + @Test(timeout = 8000) + @SuppressWarnings("unchecked") + public void testGetBlockShutdownPaths() throws Exception { + long origID = atomicLong("ID").get(); + long origRemote = atomicLong("remoteBlockNum").get(); + Field clientField = getField("databaseGrpcClient"); + Field delegateField = getField("tronNetDelegate"); + Object origClient = clientField.get(solidityNode); + Object origDelegate = delegateField.get(solidityNode); + + LinkedBlockingDeque queue = + (LinkedBlockingDeque) getField("blockQueue").get(solidityNode); + try { + // ── Part 1: interrupt during blockQueue.put() ────────────────────────── + // Fill the queue to capacity so the next put() call blocks. + for (int i = 0; i < 100; i++) { + queue.offer(blockWithNum(i)); + } + assertEquals(100, queue.size()); + + atomicLong("ID").set(0L); + atomicLong("remoteBlockNum").set(10L); + + DatabaseGrpcClient putClient = mock(DatabaseGrpcClient.class); + Mockito.when(putClient.getBlock(1L)).thenReturn(blockWithNum(1L)); + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + clientField.set(solidityNode, putClient); + delegateField.set(solidityNode, mockDelegate); + + Method getBlockM = SolidityNode.class.getDeclaredMethod("getBlock"); + getBlockM.setAccessible(true); + Thread t = new Thread(() -> { + try { + getBlockM.invoke(solidityNode); + } catch (Exception e) { + Thread.currentThread().interrupt(); + } + }); + t.start(); + Thread.sleep(200); // let the thread block inside blockQueue.put() + t.interrupt(); // simulate ExecutorService.shutdownNow() + t.join(4000); + assertFalse("getBlock must exit cleanly when interrupted during put()", t.isAlive()); + queue.clear(); + setFlag(true); + + // ── Part 2: exception while flag is false ────────────────────────────── + atomicLong("ID").set(0L); + atomicLong("remoteBlockNum").set(10L); + + DatabaseGrpcClient closingClient = mock(DatabaseGrpcClient.class); + Mockito.when(closingClient.getBlock(1L)).thenAnswer(inv -> { + setFlag(false); // shutdown races with this gRPC call + throw new RuntimeException("channel closed during shutdown"); + }); + clientField.set(solidityNode, closingClient); + delegateField.set(solidityNode, mockDelegate); + + // Must return without throwing and without infinite retry. + getBlockM.invoke(solidityNode); } finally { setFlag(true); queue.clear(); @@ -423,7 +623,7 @@ public void testGetBlockProcessesOneBlock() throws Exception { } } - // ── processSolidityBlock() ─────────────────────────────────────────────────── + // ── processSolidityBlock() ──────────────────────────────────────────────────── /** * processSolidityBlock() must drain a block from the queue, process it, and @@ -498,129 +698,6 @@ public void testProcessSolidityBlockHandlesInterrupt() throws Exception { } } - // ── loopProcessBlock() retry path ──────────────────────────────────────────── - - /** - * When pushVerifiedBlock throws, loopProcessBlock() must retry after sleeping, - * re-fetching the block via getBlockByNum, and ultimately succeed. - */ - @Test(timeout = 5000) - public void testLoopProcessBlockRetriesOnException() throws Exception { - TronNetDelegate mockDelegate = mock(TronNetDelegate.class); - Mockito.when(mockDelegate.isHitDown()).thenReturn(false); - Mockito.doThrow(new RuntimeException("push failed")) - .doNothing() - .when(mockDelegate).pushVerifiedBlock(Mockito.any()); - - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getBlock(33L)).thenReturn(blockWithNum(33L)); - - long origSolidified = chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum(); - Field delegateField = getField("tronNetDelegate"); - Field clientField = getField("databaseGrpcClient"); - Object origDelegate = delegateField.get(solidityNode); - Object origClient = clientField.get(solidityNode); - delegateField.set(solidityNode, mockDelegate); - clientField.set(solidityNode, mockClient); - try { - invokeLoopProcessBlock(blockWithNum(33L)); - assertEquals(33L, chainBaseManager.getDynamicPropertiesStore() - .getLatestSolidifiedBlockNum()); - } catch (RuntimeException e) { - Assert.assertTrue(e.getMessage().contains("push failed")); - } finally { - chainBaseManager.getDynamicPropertiesStore() - .saveLatestSolidifiedBlockNum(origSolidified); - delegateField.set(solidityNode, origDelegate); - clientField.set(solidityNode, origClient); - } - } - - // ── getBlockByNum() retry paths ────────────────────────────────────────────── - - /** - * When the returned block number does not match, getBlockByNum() must warn - * and retry; it must throw RuntimeException when flag becomes false. - */ - @Test(timeout = 5000) - public void testGetBlockByNumWarnOnWrongNum() throws Exception { - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getBlock(9L)).thenAnswer(inv -> { - setFlag(false); // cause the retry loop to exit - return blockWithNum(999L); // deliberately wrong number - }); - - Field clientField = getField("databaseGrpcClient"); - Object orig = clientField.get(solidityNode); - clientField.set(solidityNode, mockClient); - try { - Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); - m.setAccessible(true); - try { - m.invoke(solidityNode, 9L); - Assert.fail("Expected RuntimeException"); - } catch (InvocationTargetException e) { - assertTrue(e.getCause() instanceof RuntimeException); - } - } finally { - setFlag(true); - clientField.set(solidityNode, orig); - } - } - - /** - * When the gRPC call throws, getBlockByNum() must log, sleep, and retry; - * on the second attempt it must return the correct block. - */ - @Test(timeout = 5000) - public void testGetBlockByNumRetriesOnException() throws Exception { - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getBlock(8L)) - .thenThrow(new RuntimeException("rpc error")) - .thenReturn(blockWithNum(8L)); - - Field clientField = getField("databaseGrpcClient"); - Object orig = clientField.get(solidityNode); - clientField.set(solidityNode, mockClient); - try { - Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); - m.setAccessible(true); - Block result = (Block) m.invoke(solidityNode, 8L); - assertEquals(8L, result.getBlockHeader().getRawData().getNumber()); - } finally { - clientField.set(solidityNode, orig); - } - } - - // ── getLastSolidityBlockNum() retry path ───────────────────────────────────── - - /** - * When getDynamicProperties() throws, getLastSolidityBlockNum() must log, - * sleep, and retry; on the second attempt it must return the fetched value. - */ - @Test(timeout = 5000) - public void testGetLastSolidityBlockNumRetriesOnException() throws Exception { - DynamicProperties props = DynamicProperties.newBuilder() - .setLastSolidityBlockNum(50L).build(); - DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); - Mockito.when(mockClient.getDynamicProperties()) - .thenThrow(new RuntimeException("rpc error")) - .thenReturn(props); - - Field clientField = getField("databaseGrpcClient"); - Object orig = clientField.get(solidityNode); - clientField.set(solidityNode, mockClient); - try { - Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); - m.setAccessible(true); - long result = (long) m.invoke(solidityNode); - assertEquals(50L, result); - } finally { - clientField.set(solidityNode, orig); - } - } - // ── private helpers ────────────────────────────────────────────────────────── private static Field getField(String name) throws Exception { diff --git a/framework/src/test/resources/args-test.conf b/framework/src/test/resources/args-test.conf deleted file mode 100644 index db889483270..00000000000 --- a/framework/src/test/resources/args-test.conf +++ /dev/null @@ -1,223 +0,0 @@ -net { - # type is deprecated and has no effect. - # type = mainnet -} - - -storage { - # Directory for storing persistent data - - db.engine = "LEVELDB" - db.directory = "database", - index.directory = "index", - - # You can custom these 14 databases' configs: - - # account, account-index, asset-issue, block, block-index, - # block_KDB, peers, properties, recent-block, trans, - # utxo, votes, witness, witness_schedule. - - # Otherwise, db configs will remain defualt and data will be stored in - # the path of "output-directory" or which is set by "-d" ("--output-directory"). - - # Attention: name is a required field that must be set !!! - default = { - maxOpenFiles = 50 - } - defaultM = { - maxOpenFiles = 500 - } - defaultL = { - maxOpenFiles = 1000 - } - properties = [ - { - name = "account", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - { - name = "account-index", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - { # only for unit test - name = "test_name", - path = "test_path", - createIfMissing = false, - paranoidChecks = false, - verifyChecksums = false, - compressionType = 1, - blockSize = 2, - writeBufferSize = 3, - cacheSize = 4, - maxOpenFiles = 5 - }, - ] - - needToUpdateAsset = false - -} - -node.discovery = { - enable = true - persist = true - external.ip = "46.168.1.1" -} - -node { - - trustNode = "127.0.0.1:50051" - - listen.port = 18888 - - active = [] - - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - inactiveThreshold = 600 //seconds - - p2p { - version = 43 # 43: testnet; 101: debug - } - - rpc { - port = 50051 - } - -} - -sync { - node.count = 30 -} - -seed.node = { - ip.list = [ - ] -} - -genesis.block = { - # Reserve balance - assets = [ - { - accountName = "Devaccount" - accountType = "AssetIssue" - address = "27d3byPxZXKQWfXX7sJvemJJuv5M65F3vjS" - balance = "10000000000000000" - }, - { - accountName = "Zion" - accountType = "AssetIssue" - address = "27fXgQ46DcjEsZ444tjZPKULcxiUfDrDjqj" - balance = "15000000000000000" - }, - { - accountName = "Sun" - accountType = "AssetIssue" - address = "27SWXcHuQgFf9uv49FknBBBYBaH3DUk4JPx" - balance = "10000000000000000" - }, - { - accountName = "Blackhole" - accountType = "AssetIssue" - address = "27WtBq2KoSy5v8VnVZBZHHJcDuWNiSgjbE3" - balance = "-9223372036854775808" - } - ] - - witnesses = [ - { - address: 27Ssb1WE8FArwJVRRb8Dwy3ssVGuLY8L3S1 - url = "http://Mercury.org", - voteCount = 105 - }, - { - address: 27anh4TDZJGYpsn4BjXzb7uEArNALxwiZZW - url = "http://Venus.org", - voteCount = 104 - }, - { - address: 27Wkfa5iEJtsKAKdDzSmF1b2gDm5s49kvdZ - url = "http://Earth.org", - voteCount = 103 - }, - { - address: 27bqKYX9Bgv7dgTY7xBw5SUHZ8EGaPSikjx - url = "http://Mars.org", - voteCount = 102 - }, - { - address: 27fASUY6qKtsaAEPz6QxhZac2KYVz2ZRTXW - url = "http://Jupiter.org", - voteCount = 101 - }, - { - address: 27Q3RSbiqm59VXcF8shQWHKbyztfso5FwvP - url = "http://Saturn.org", - voteCount = 100 - }, - { - address: 27YkUVSuvCK3K84DbnFnxYUxozpi793PTqZ - url = "http://Uranus.org", - voteCount = 99 - }, - { - address: 27kdTBTDJ16hK3Xqr8PpCuQJmje1b94CDJU - url = "http://Neptune.org", - voteCount = 98 - }, - { - address: 27mw9UpRy7inTMQ5kUzsdTc2QZ6KvtCX4uB - url = "http://Pluto.org", - voteCount = 97 - }, - { - address: 27QzC4PeQZJ2kFMUXiCo4S8dx3VWN5U9xcg - url = "http://Altair.org", - voteCount = 96 - }, - { - address: 27VZHn9PFZwNh7o2EporxmLkpe157iWZVkh - url = "http://AlphaLyrae.org", - voteCount = 95 - } - ] - - timestamp = "0" #2017-8-26 12:00:00 - - parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" -} - - -localwitness = [ - -] - -block = { - needSyncCheck = true # first node : false, other : true -} - -vm = { - supportConstant = true - minTimeRatio = 0.0 - maxTimeRatio = 5.0 -} -committee = { - allowCreationOfContracts = 1 //mainnet:0 (reset by committee),test:1 - allowOldRewardOpt = 1 -} diff --git a/framework/src/test/resources/config-localtest.conf b/framework/src/test/resources/config-shield.conf similarity index 62% rename from framework/src/test/resources/config-localtest.conf rename to framework/src/test/resources/config-shield.conf index 4c6910e3d7a..1c185f8f82f 100644 --- a/framework/src/test/resources/config-localtest.conf +++ b/framework/src/test/resources/config-shield.conf @@ -7,13 +7,6 @@ storage { # Directory for storing persistent data db.engine ="LEVELDB", db.directory = "database", - index.directory = "index", - - # This configuration item is only for SolidityNode. - # Turn off the index is "off", else "on". - # Turning off the index will significantly improve the performance of the SolidityNode sync block. - # You can turn off the index if you don't use the two interfaces getTransactionsToThis and getTransactionsFromThis. - index.switch = "on" # You can custom these 14 databases' configs: @@ -26,30 +19,6 @@ storage { # Attention: name is a required field that must be set !!! properties = [ - // { - // name = "account", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, - // { - // name = "account-index", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, ] } @@ -169,18 +138,7 @@ node { seed.node = { - # List of the seed nodes - # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] ip.list = [ - "127.0.0.1:6666", - // "127.0.0.1:7777", - // "127.0.0.1:8888", - // "127.0.0.1:9999", ] } @@ -218,23 +176,6 @@ genesis.block = { url = "http://Test.org", voteCount = 106 }, - // { - // address: TPrLL5ckUdMaPNgJYmGv23qtYjBE34aBf8 - // url = "http://Mercury.org", - // voteCount = 105 - // }, - // { - // address: TEZBh76rouEQpB2zqYVopbRXGx7RfyWorT - // #address: 27TfVERREG3FeWMHEAQ95tWHG4sb3ANn3Qe - // url = "http://Venus.org", - // voteCount = 104 - // }, - // { - // address: TN27wbfCLEN1gP2PZAxHgU3QZrntsLyxdj - // #address: 27b8RUuyZnNPFNZGct2bZkNu9MnGWNAdH3Z - // url = "http://Earth.org", - // voteCount = 103 - // }, ] timestamp = "0" #2017-8-26 12:00:00 @@ -242,12 +183,6 @@ genesis.block = { parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" } -// Optional.The default is empty. -// It is used when the witness account has set the witnessPermission. -// When it is not empty, the localWitnessAccountAddress represents the address of the witness account, -// and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. -// When it is empty,the localwitness is configured with the private key of the witness account. - //localWitnessAccountAddress = TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz localwitness = [ @@ -281,9 +216,3 @@ committee = { allowTvmConstantinople = 1 allowTvmSolidity059 = 1 } - -log.level = { - root = "INFO" // TRACE;DEBUG;INFO;WARN;ERROR - allowCreationOfContracts = 1 //mainnet:0 (reset by committee),test:1 - allowMultiSign = 1 //mainnet:0 (reset by committee),test:1 -} diff --git a/framework/src/test/resources/config-test-index.conf b/framework/src/test/resources/config-test-index.conf deleted file mode 100644 index 583064a37f5..00000000000 --- a/framework/src/test/resources/config-test-index.conf +++ /dev/null @@ -1,174 +0,0 @@ -net { - # type is deprecated and has no effect. - # type = mainnet -} - - -storage { - # Directory for storing persistent data - - db.directory = "database", - index.directory = "index", - - # You can custom these 14 databases' configs: - - # account, account-index, asset-issue, block, block-index, - # block_KDB, peers, properties, recent-block, trans, - # utxo, votes, witness, witness_schedule. - - # Otherwise, db configs will remain defualt and data will be stored in - # the path of "output-directory" or which is set by "-d" ("--output-directory"). - - # Attention: name is a required field that must be set !!! - properties = [ - { - name = "account", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - { - name = "account-index", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - ] - - needToUpdateAsset = false - -} - -node.discovery = { - enable = true - persist = true - external.ip = "" -} - -node { - listen.port = 18888 - - active = [ - # Sample entries: - # { url = "enode://@hostname.com:30303" } - # { - # ip = hostname.com - # port = 30303 - # nodeId = e437a4836b77ad9d9ffe73ee782ef2614e6d8370fcf62191a6e488276e23717147073a7ce0b444d485fff5a0c34c4577251a7a990cf80d8542e21b95aa8c5e6c - # } - ] - - p2p { - version = 43 # 43: testnet; 101: debug - } - - http { - fullNodeEnable = false - solidityEnable = false - PBFTEnable = false - } - - jsonrpc { - httpFullNodeEnable = false - httpSolidityEnable = false - httpPBFTEnable = false - # maxBlockRange = 5000 - # maxSubTopics = 1000 - # maxBlockFilterNum = 30000 - } - - rpc { - port = 50051 - enable = false - solidityEnable = false - PBFTEnable = false - # Number of gRPC thread, default availableProcessors / 2 - # thread = 16 - - # The maximum number of concurrent calls permitted for each incoming connection - # maxConcurrentCallsPerConnection = - - # The HTTP/2 flow control window, default 1MB - # flowControlWindow = - - # Connection being idle for longer than which will be gracefully terminated - maxConnectionIdleInMillis = 60000 - - # Connection lasting longer than which will be gracefully terminated - # maxConnectionAgeInMillis = - - # The maximum message size allowed to be received on the server, default 4MB - # maxMessageSize = - - # The maximum size of header list allowed to be received, default 8192 - # maxHeaderListSize = - } - -} - -sync { - node.count = 30 -} - -seed.node = { - # List of the seed nodes - # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] - ip.list = [ - "47.254.16.55:18888", - "47.254.18.49:18888", - "18.188.111.53:18888", - "54.219.41.56:18888", - "35.169.113.187:18888", - "34.214.241.188:18888", - "47.254.146.147:18888", - "47.254.144.25:18888", - "47.91.246.252:18888", - "47.91.216.69:18888", - "39.106.220.120:18888" - ] -} - -genesis.block = { - # Reserve balance - assets = [ - { - accountName = "Blackhole" - accountType = "AssetIssue" - address = "THmtHi1Rzq4gSKYGEKv1DPkV7au6xU1AUB" - balance = "-9223372036854775808" - } - ] - - witnesses = [ - - ] - - timestamp = "0" #2017-8-26 12:00:00 - - parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" -} - -localwitness = [ - -] - -block = { - needSyncCheck = true # first node : false, other : true -} diff --git a/framework/src/test/resources/config-test-mainnet.conf b/framework/src/test/resources/config-test-mainnet.conf deleted file mode 100644 index 938812f8214..00000000000 --- a/framework/src/test/resources/config-test-mainnet.conf +++ /dev/null @@ -1,242 +0,0 @@ -net { - # type is deprecated and has no effect. - # type = mainnet -} - - -storage { - # Directory for storing persistent data - - db.directory = "database", - index.directory = "index", - - # You can custom these 14 databases' configs: - - # account, account-index, asset-issue, block, block-index, - # block_KDB, peers, properties, recent-block, trans, - # utxo, votes, witness, witness_schedule. - - # Otherwise, db configs will remain defualt and data will be stored in - # the path of "output-directory" or which is set by "-d" ("--output-directory"). - - # Attention: name is a required field that must be set !!! - properties = [ - // { - // name = "account", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, - // { - // name = "account-index", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, - ] - - needToUpdateAsset = false -} - -node.discovery = { - enable = true - persist = true - external.ip = "46.168.1.1" -} - -node { - - trustNode = "127.0.0.1:50051" - - listen.port = 18888 - - active = [ - # Sample entries: - # { url = "enode://@hostname.com:30303" } - # { - # ip = hostname.com - # port = 30303 - # nodeId = e437a4836b77ad9d9ffe73ee782ef2614e6d8370fcf62191a6e488276e23717147073a7ce0b444d485fff5a0c34c4577251a7a990cf80d8542e21b95aa8c5e6c - # } - ] - - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - - p2p { - version = 43 # 43: testnet; 101: debug - } - - http { - fullNodeEnable = false - solidityEnable = false - PBFTEnable = false - } - - jsonrpc { - httpFullNodeEnable = false - httpSolidityEnable = false - httpPBFTEnable = false - # maxBlockRange = 5000 - # maxSubTopics = 1000 - # maxBlockFilterNum = 50000 - # maxLogFilterNum = 20000 - } - - rpc { - enable = false - solidityEnable = false - PBFTEnable = false - } - -} - -sync { - node.count = 30 -} - -seed.node = { - # List of the seed nodes - # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] - ip.list = [ - "47.254.16.55:18888", - "47.254.18.49:18888", - "18.188.111.53:18888", - "54.219.41.56:18888", - "35.169.113.187:18888", - "34.214.241.188:18888", - "47.254.146.147:18888", - "47.254.144.25:18888", - "47.91.246.252:18888", - "47.91.216.69:18888", - "39.106.220.120:18888" - ] -} - -genesis.block = { - # Reserve balance - assets = [ - # { - # accountName = "tron" - # accountType = "AssetIssue" # Normal/AssetIssue/Contract - # address = "TFveVqgQKAdFa12DNnXTw7GHCDQK7fUVen" - # balance = "10" - # } - { - accountName = "Devaccount" - accountType = "AssetIssue" - address = "TPwJS5eC5BPGyMGtYTHNhPTB89sUWjDSSu" - balance = "10000000000000000" - }, - { - accountName = "Zion" - accountType = "AssetIssue" - address = "TSRNrjmrAbDdrsoqZsv7FZUtAo13fwoCzv" - balance = "15000000000000000" - }, - { - accountName = "Sun" - accountType = "AssetIssue" - address = "TDQE4yb3E7dvDjouvu8u7GgSnMZbxAEumV" - balance = "10000000000000000" - }, - { - accountName = "Blackhole" - accountType = "AssetIssue" - address = "THmtHi1Rzq4gSKYGEKv1DPkV7au6xU1AUB" - balance = "-9223372036854775808" - } - ] - - witnesses = [ - { - address: TDmHUBuko2qhcKBCGGafu928hMRj1tX2RW - url = "http://Mercury.org", - voteCount = 105 - }, - { - address: TMgPX8uBr8XbBboxQgMK3zNS4SgjUa3eiP - url = "http://Venus.org", - voteCount = 104 - }, - { - address: THeN2mPrrkr5U9Nzfb7xwgAwRqcFWcL7pR - url = "http://Earth.org", - voteCount = 103 - }, - { - address: TNj21CppEn6PzHHtdLHoNZRpLJnxogNnAX - url = "http://Mars.org", - voteCount = 102 - }, - { - address: TS48wDnTskrLU49kmZKRVfkHXd2NQ3dZP4 - url = "http://Jupiter.org", - voteCount = 101 - }, - { - address: TAw7uHQUJw8FqRzuYqmEDQkFCyCGE4JcsW - url = "http://Saturn.org", - voteCount = 100 - }, - { - address: TKeAx8bYkB25RsyNTQ9gUa75CuEVfFbF6N - url = "http://Uranus.org", - voteCount = 99 - }, - { - address: TXX9e8tvYxg5MMbcoYAvqVT2wiXyacjs65 - url = "http://Neptune.org", - voteCount = 98 - }, - { - address: TYpqwW7bfamDfDqXA9EMPhAfmArKMicxp9 - url = "http://Pluto.org", - voteCount = 97 - }, - { - address: TBstX5L37A1WZBEJPM9nNDnDFa2kcTVSmc - url = "http://Altair.org", - voteCount = 96 - }, - { - address: TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W - url = "http://AlphaLyrae.org", - voteCount = 95 - } - ] - - timestamp = "0" #2017-8-26 12:00:00 - - parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" -} - -localwitness = [ - -] - -block = { - needSyncCheck = true # first node : false, other : true -} - -committee = { - allowCreationOfContracts = 1 //mainnet:0 (reset by committee),test:1 -} diff --git a/framework/src/test/resources/config-test-storagetest.conf b/framework/src/test/resources/config-test-storagetest.conf deleted file mode 100644 index 113c8371ba1..00000000000 --- a/framework/src/test/resources/config-test-storagetest.conf +++ /dev/null @@ -1,287 +0,0 @@ -net { - # type is deprecated and has no effect. - # type = mainnet -} - - -storage { - # Directory for storing persistent data - - db.engine = "LEVELDB" - db.directory = "database", - index.directory = "index", - - # You can custom these 14 databases' configs: - - # account, account-index, asset-issue, block, block-index, - # block_KDB, peers, properties, recent-block, trans, - # utxo, votes, witness, witness_schedule. - - # Otherwise, db configs will remain defualt and data will be stored in - # the path of "output-directory" or which is set by "-d" ("--output-directory"). - - # Attention: name is a required field that must be set !!! - default = { - maxOpenFiles = 50 - } - defaultM = { - maxOpenFiles = 500 - } - defaultL = { - maxOpenFiles = 1000 - } - properties = [ - { - name = "account", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - { - name = "account-index", - path = "storage_directory_test", - createIfMissing = true, - 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 - }, - { # only for unit test - name = "test_name", - path = "test_path", - createIfMissing = false, - paranoidChecks = false, - verifyChecksums = false, - compressionType = 1, - blockSize = 2, - writeBufferSize = 3, - cacheSize = 4, - maxOpenFiles = 5 - }, - ] - - needToUpdateAsset = false - -} - -node.discovery = { - enable = true - persist = true - external.ip = "46.168.1.1" -} - -node { - - trustNode = "127.0.0.1:50051" - - listen.port = 18888 - - active = [ - # Sample entries: - # { url = "enode://@hostname.com:30303" } - # { - # ip = hostname.com - # port = 30303 - # nodeId = e437a4836b77ad9d9ffe73ee782ef2614e6d8370fcf62191a6e488276e23717147073a7ce0b444d485fff5a0c34c4577251a7a990cf80d8542e21b95aa8c5e6c - # } - ] - - maxConnections = 30 - minConnections = 8 - minActiveConnections = 3 - - p2p { - version = 43 # 43: testnet; 101: debug - } - - rpc { - port = 50051 - - # Number of gRPC thread, default availableProcessors / 2 - # thread = 16 - - # The maximum number of concurrent calls permitted for each incoming connection - # maxConcurrentCallsPerConnection = - - # The HTTP/2 flow control window, default 1MB - # flowControlWindow = - - # Connection being idle for longer than which will be gracefully terminated - maxConnectionIdleInMillis = 60000 - - # Connection lasting longer than which will be gracefully terminated - # maxConnectionAgeInMillis = - - # The maximum message size allowed to be received on the server, default 4MB - # maxMessageSize = - - # The maximum size of header list allowed to be received, default 8192 - # maxHeaderListSize = - } - -} - -sync { - node.count = 30 -} - -seed.node = { - # List of the seed nodes - # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] - ip.list = [ - "47.254.16.55:18888", - "47.254.18.49:18888", - "18.188.111.53:18888", - "54.219.41.56:18888", - "35.169.113.187:18888", - "34.214.241.188:18888", - "47.254.146.147:18888", - "47.254.144.25:18888", - "47.91.246.252:18888", - "47.91.216.69:18888", - "39.106.220.120:18888" - ] -} - -genesis.block = { - # Reserve balance - assets = [ - # { - # accountName = "tron" - # accountType = "AssetIssue" # Normal/AssetIssue/Contract - # address = "TFveVqgQKAdFa12DNnXTw7GHCDQK7fUVen" - # balance = "10" - # } - { - accountName = "Devaccount" - accountType = "AssetIssue" - address = "TPwJS5eC5BPGyMGtYTHNhPTB89sUWjDSSu" - balance = "10000000000000000" - }, - { - accountName = "Zion" - accountType = "AssetIssue" - address = "TSRNrjmrAbDdrsoqZsv7FZUtAo13fwoCzv" - balance = "15000000000000000" - }, - { - accountName = "Sun" - accountType = "AssetIssue" - address = "TDQE4yb3E7dvDjouvu8u7GgSnMZbxAEumV" - balance = "10000000000000000" - }, - { - accountName = "Blackhole" - accountType = "AssetIssue" - address = "THmtHi1Rzq4gSKYGEKv1DPkV7au6xU1AUB" - balance = "-9223372036854775808" - } - ] - - witnesses = [ - { - address: TDmHUBuko2qhcKBCGGafu928hMRj1tX2RW - url = "http://Mercury.org", - voteCount = 105 - }, - { - address: TMgPX8uBr8XbBboxQgMK3zNS4SgjUa3eiP - url = "http://Venus.org", - voteCount = 104 - }, - { - address: THeN2mPrrkr5U9Nzfb7xwgAwRqcFWcL7pR - url = "http://Earth.org", - voteCount = 103 - }, - { - address: TNj21CppEn6PzHHtdLHoNZRpLJnxogNnAX - url = "http://Mars.org", - voteCount = 102 - }, - { - address: TS48wDnTskrLU49kmZKRVfkHXd2NQ3dZP4 - url = "http://Jupiter.org", - voteCount = 101 - }, - { - address: TAw7uHQUJw8FqRzuYqmEDQkFCyCGE4JcsW - url = "http://Saturn.org", - voteCount = 100 - }, - { - address: TKeAx8bYkB25RsyNTQ9gUa75CuEVfFbF6N - url = "http://Uranus.org", - voteCount = 99 - }, - { - address: TXX9e8tvYxg5MMbcoYAvqVT2wiXyacjs65 - url = "http://Neptune.org", - voteCount = 98 - }, - { - address: TYpqwW7bfamDfDqXA9EMPhAfmArKMicxp9 - url = "http://Pluto.org", - voteCount = 97 - }, - { - address: TBstX5L37A1WZBEJPM9nNDnDFa2kcTVSmc - url = "http://Altair.org", - voteCount = 96 - }, - { - address: TGSzEq4t7oMTRcn1VxDghRu5r5bWAE5D1W - url = "http://AlphaLyrae.org", - voteCount = 95 - } - ] - - timestamp = "0" #2017-8-26 12:00:00 - - parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" -} - - -// Optional.The default is empty. -// It is used when the witness account has set the witnessPermission. -// When it is not empty, the localWitnessAccountAddress represents the address of the witness account, -// and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. -// When it is empty,the localwitness is configured with the private key of the witness account. - -//localWitnessAccountAddress = - -localwitness = [ - -] - -block = { - needSyncCheck = true # first node : false, other : true -} - -vm = { - supportConstant = true - minTimeRatio = 0.0 - maxTimeRatio = 5.0 - - # In rare cases, transactions that will be within the specified maximum execution time (default 10(ms)) are re-executed and packaged - # longRunningTime = 10 -} -committee = { - allowCreationOfContracts = 1 //mainnet:0 (reset by committee),test:1 - allowOldRewardOpt = 1 - allowNewRewardAlgorithm = 1 -} diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 85172c37710..a7bf77654cb 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -3,13 +3,11 @@ net { # type = mainnet } - storage { # Directory for storing persistent data db.engine = "LEVELDB" db.directory = "database", - index.directory = "index", # You can custom these 14 databases' configs: @@ -20,44 +18,17 @@ storage { # Otherwise, db configs will remain defualt and data will be stored in # the path of "output-directory" or which is set by "-d" ("--output-directory"). - # Attention: name is a required field that must be set !!! + # Per-database storage path overrides (name is required; see reference.conf for full property list). properties = [ - // { - // name = "account", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, - // { - // name = "account-index", - // path = "storage_directory_test", - // createIfMissing = true, - // 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 - // }, - // { # only for unit test - // name = "test_name", - // path = "test_path", - // createIfMissing = false, - // paranoidChecks = false, - // verifyChecksums = false, - // compressionType = 1, - // blockSize = 2, - // writeBufferSize = 3, - // cacheSize = 4, - // maxOpenFiles = 5 - // }, + # { + # name = "account", + # path = "storage_directory_test", + # # 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 + # }, ] needToUpdateAsset = false @@ -90,13 +61,6 @@ node { listen.port = 18888 active = [ - # Sample entries: - # { url = "enode://@hostname.com:30303" } - # { - # ip = hostname.com - # port = 30303 - # nodeId = e437a4836b77ad9d9ffe73ee782ef2614e6d8370fcf62191a6e488276e23717147073a7ce0b444d485fff5a0c34c4577251a7a990cf80d8542e21b95aa8c5e6c - # } ] inactiveThreshold = 600 //seconds @@ -220,25 +184,7 @@ sync { } seed.node = { - # List of the seed nodes - # Seed nodes are stable full nodes - # example: - # ip.list = [ - # "ip:port", - # "ip:port" - # ] ip.list = [ - "47.254.16.55:18888", - "47.254.18.49:18888", - "18.188.111.53:18888", - "54.219.41.56:18888", - "35.169.113.187:18888", - "34.214.241.188:18888", - "47.254.146.147:18888", - "47.254.144.25:18888", - "47.91.246.252:18888", - "47.91.216.69:18888", - "39.106.220.120:18888" ] } @@ -340,17 +286,9 @@ genesis.block = { parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000" } - -// Optional.The default is empty. -// It is used when the witness account has set the witnessPermission. -// When it is not empty, the localWitnessAccountAddress represents the address of the witness account, -// and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. -// When it is empty,the localwitness is configured with the private key of the witness account. - //localWitnessAccountAddress = localwitness = [ - ] block = { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8c55e3b52b0..832d2728f0b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1084,15 +1084,15 @@ - - - + + + - - + + - - + + diff --git a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java index 960c1414769..4ee7567ec28 100644 --- a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java @@ -15,6 +15,7 @@ import org.junit.Rule; import org.junit.rules.TemporaryFolder; import org.tron.api.WalletGrpc; +import org.tron.common.TestConstants; import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; @@ -74,7 +75,7 @@ public void init(String dbType, boolean historyBalanceLookup) throws IOException dbPath = folder.newFolder().toString(); Args.setParam(new String[] { "-d", dbPath, "-w", "--p2p-disable", "true", "--storage-db-engine", dbType}, - "config-localtest.conf"); + TestConstants.SHIELD_CONF); // allow account root Args.getInstance().setAllowAccountStateRoot(1); Args.getInstance().setRpcPort(PublicMethod.chooseRandomPort()); diff --git a/plugins/src/test/resources/config-duplicate.conf b/plugins/src/test/resources/config-duplicate.conf index f2eb7fbf357..05358016810 100644 --- a/plugins/src/test/resources/config-duplicate.conf +++ b/plugins/src/test/resources/config-duplicate.conf @@ -4,7 +4,6 @@ storage { db.engine = "LEVELDB", db.sync = false, db.directory = "database", - index.directory = "index", transHistory.switch = "on", properties = [ { diff --git a/protocol/src/main/protos/api/api.proto b/protocol/src/main/protos/api/api.proto index 6082d989182..f8d13a6bbd3 100644 --- a/protocol/src/main/protos/api/api.proto +++ b/protocol/src/main/protos/api/api.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package protocol; import "core/Tron.proto"; -import "google/api/annotations.proto"; import "core/contract/asset_issue_contract.proto"; import "core/contract/account_contract.proto"; @@ -17,7 +16,7 @@ import "core/contract/shield_contract.proto"; option java_package = "org.tron.api"; //Specify the name of the package that generated the Java file option java_outer_classname = "GrpcAPI"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/api"; +option go_package = "github.com/tronprotocol/grpc-gateway/api"; service Wallet { diff --git a/protocol/src/main/protos/api/zksnark.proto b/protocol/src/main/protos/api/zksnark.proto index 4bbca3b3964..bc0764cb529 100644 --- a/protocol/src/main/protos/api/zksnark.proto +++ b/protocol/src/main/protos/api/zksnark.proto @@ -5,7 +5,7 @@ import "core/Tron.proto"; option java_package = "org.tron.api"; //Specify the name of the package that generated the Java file option java_outer_classname = "ZksnarkGrpcAPI"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/api"; +option go_package = "github.com/tronprotocol/grpc-gateway/api"; service TronZksnark { rpc CheckZksnarkProof (ZksnarkRequest) returns (ZksnarkResponse) { diff --git a/protocol/src/main/protos/core/Discover.proto b/protocol/src/main/protos/core/Discover.proto index c455c96af72..b9190812791 100644 --- a/protocol/src/main/protos/core/Discover.proto +++ b/protocol/src/main/protos/core/Discover.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos"; //Specify the name of the package that generated the Java file option java_outer_classname = "Discover"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message Endpoint { bytes address = 1; diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 6a294c32b0c..a68e841bb60 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -8,7 +8,7 @@ package protocol; option java_package = "org.tron.protos"; //Specify the name of the package that generated the Java file option java_outer_classname = "Protocol"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; enum AccountType { Normal = 0; diff --git a/protocol/src/main/protos/core/TronInventoryItems.proto b/protocol/src/main/protos/core/TronInventoryItems.proto index 9dde38fb34c..a82d2de4552 100644 --- a/protocol/src/main/protos/core/TronInventoryItems.proto +++ b/protocol/src/main/protos/core/TronInventoryItems.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos"; //Specify the name of the package that generated the Java file option java_outer_classname = "TronInventoryItems"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message InventoryItems { int32 type = 1; diff --git a/protocol/src/main/protos/core/contract/account_contract.proto b/protocol/src/main/protos/core/contract/account_contract.proto index 6f85441dd26..d3180048f43 100644 --- a/protocol/src/main/protos/core/contract/account_contract.proto +++ b/protocol/src/main/protos/core/contract/account_contract.proto @@ -19,7 +19,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "Contract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; import "core/Tron.proto"; diff --git a/protocol/src/main/protos/core/contract/asset_issue_contract.proto b/protocol/src/main/protos/core/contract/asset_issue_contract.proto index 79800c73e53..9e8ff463d52 100644 --- a/protocol/src/main/protos/core/contract/asset_issue_contract.proto +++ b/protocol/src/main/protos/core/contract/asset_issue_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "AssetIssueContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message AssetIssueContract { string id = 41; diff --git a/protocol/src/main/protos/core/contract/balance_contract.proto b/protocol/src/main/protos/core/contract/balance_contract.proto index 2bc6fafd40d..ea1c96270d6 100644 --- a/protocol/src/main/protos/core/contract/balance_contract.proto +++ b/protocol/src/main/protos/core/contract/balance_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "FreezeBalanceContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; import "core/contract/common.proto"; diff --git a/protocol/src/main/protos/core/contract/common.proto b/protocol/src/main/protos/core/contract/common.proto index ba125e131f2..8af929bd52d 100644 --- a/protocol/src/main/protos/core/contract/common.proto +++ b/protocol/src/main/protos/core/contract/common.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "common"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; enum ResourceCode { BANDWIDTH = 0x00; diff --git a/protocol/src/main/protos/core/contract/exchange_contract.proto b/protocol/src/main/protos/core/contract/exchange_contract.proto index 4d4cc185810..8b8878f04f5 100644 --- a/protocol/src/main/protos/core/contract/exchange_contract.proto +++ b/protocol/src/main/protos/core/contract/exchange_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "ExchangeCreateContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message ExchangeCreateContract { bytes owner_address = 1; diff --git a/protocol/src/main/protos/core/contract/market_contract.proto b/protocol/src/main/protos/core/contract/market_contract.proto index 310fcacf217..e1274350036 100644 --- a/protocol/src/main/protos/core/contract/market_contract.proto +++ b/protocol/src/main/protos/core/contract/market_contract.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message MarketSellAssetContract { bytes owner_address = 1; diff --git a/protocol/src/main/protos/core/contract/proposal_contract.proto b/protocol/src/main/protos/core/contract/proposal_contract.proto index 126790ca874..35bb9ca7647 100644 --- a/protocol/src/main/protos/core/contract/proposal_contract.proto +++ b/protocol/src/main/protos/core/contract/proposal_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "ProposalApproveContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message ProposalApproveContract { bytes owner_address = 1; diff --git a/protocol/src/main/protos/core/contract/shield_contract.proto b/protocol/src/main/protos/core/contract/shield_contract.proto index 4b2f329b73e..660f9ddf77d 100644 --- a/protocol/src/main/protos/core/contract/shield_contract.proto +++ b/protocol/src/main/protos/core/contract/shield_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "ShieldedTransferContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; // for shielded transaction diff --git a/protocol/src/main/protos/core/contract/smart_contract.proto b/protocol/src/main/protos/core/contract/smart_contract.proto index 6406cdc2a04..c913f7f7577 100644 --- a/protocol/src/main/protos/core/contract/smart_contract.proto +++ b/protocol/src/main/protos/core/contract/smart_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "CreateSmartContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; import "core/Tron.proto"; diff --git a/protocol/src/main/protos/core/contract/storage_contract.proto b/protocol/src/main/protos/core/contract/storage_contract.proto index d10f0ea041e..f04bf716e79 100644 --- a/protocol/src/main/protos/core/contract/storage_contract.proto +++ b/protocol/src/main/protos/core/contract/storage_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "BuyStorageBytesContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message BuyStorageBytesContract { bytes owner_address = 1; diff --git a/protocol/src/main/protos/core/contract/vote_asset_contract.proto b/protocol/src/main/protos/core/contract/vote_asset_contract.proto index 48930a7546e..d3b8e5b779e 100644 --- a/protocol/src/main/protos/core/contract/vote_asset_contract.proto +++ b/protocol/src/main/protos/core/contract/vote_asset_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "VoteAssetContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message VoteAssetContract { bytes owner_address = 1; diff --git a/protocol/src/main/protos/core/contract/witness_contract.proto b/protocol/src/main/protos/core/contract/witness_contract.proto index b02096cee81..5021fbf9a78 100644 --- a/protocol/src/main/protos/core/contract/witness_contract.proto +++ b/protocol/src/main/protos/core/contract/witness_contract.proto @@ -4,7 +4,7 @@ package protocol; option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file //option java_outer_classname = "WitnessCreateContract"; //Specify the class name of the generated Java file -option go_package = "github.com/tronprotocol/protocol/core/contract"; +option go_package = "github.com/tronprotocol/grpc-gateway/core"; message WitnessCreateContract { bytes owner_address = 1;