diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..d171944d877 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +./* +!docker-entrypoint.sh + diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 0b036c33f9f..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -Please answer these questions before submitting your issue. Thanks! - -1. What did you do? -If possible, provide a recipe for reproducing the error. - - -2. What did you expect to see? - - - -3. What did you see instead? - - diff --git a/.github/ISSUE_TEMPLATE/ask-a-question.md b/.github/ISSUE_TEMPLATE/ask-a-question.md new file mode 100644 index 00000000000..1517046d7da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask-a-question.md @@ -0,0 +1,51 @@ +--- +name: Ask a question +about: Something is unclear or needs clarification +title: '[Question]' +labels: 'type:docs' +assignees: '' + +--- + + + +## Question + + + +## Context + + + +**What are you trying to achieve?** + + +**What have you tried so far?** + + +**Relevant documentation or code** + + +## Environment (if applicable) + +- Network: +- java-tron version: +- Operating System: +- Java version: + +## Additional Information (Optional) + + diff --git a/.github/ISSUE_TEMPLATE/report-a-bug.md b/.github/ISSUE_TEMPLATE/report-a-bug.md new file mode 100644 index 00000000000..30a3b245862 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report-a-bug.md @@ -0,0 +1,86 @@ +--- +name: Report a bug +about: Create a report to help us improve +title: '[Bug]' +labels: 'type:bug' +assignees: '' + +--- + + + +## Bug Description + + + +## Environment + +**Network** + + +**Software Versions** + + +``` +OS: +JVM: +Git Commit: +Version: +Code: +``` + +**Configuration** + + +## Expected Behavior + + + +## Actual Behavior + + + +## Frequency + + +- [ ] Always (100%) +- [ ] Frequently (>50%) +- [ ] Sometimes (10-50%) +- [ ] Rarely (<10%) + +## Steps to Reproduce + + + +1. +2. +3. + +## Logs and Error Messages + + + +``` +[Paste error messages, stack traces, or relevant logs here] +``` + +## Additional Context (Optional) + + + +**Screenshots** + + +**Related Issues** + + +**Possible Solution** + diff --git a/.github/ISSUE_TEMPLATE/request-a-feature.md b/.github/ISSUE_TEMPLATE/request-a-feature.md new file mode 100644 index 00000000000..d8234f92a25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request-a-feature.md @@ -0,0 +1,47 @@ +--- +name: Request a feature +about: Suggest an idea for this project +title: '[Feature]' +labels: 'type:feature' +assignees: '' + +--- + +# Summary + + +# Problem +### Motivation + + +### Current State + + +### Limitations or Risks + + +# Proposed Solution + +### Proposed Design + + +### Key Changes + + +# Impact + + +# Compatibility + + +# References (Optional) + + +# Additional Notes +- Do you have ideas regarding implementation? Yes / No +- Are you willing to implement this feature? Yes / No \ No newline at end of file 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/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..9c3af93f787 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,56 @@ +name: "CodeQL" + +on: + push: + branches: [ 'develop', 'master', 'release_**' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'develop' ] + paths-ignore: [ '**/*.md', '.gitignore', '**/.gitignore', '.editorconfig', + '.gitattributes', 'docs/**', 'CHANGELOG', '.github/ISSUE_TEMPLATE/**', + '.github/PULL_REQUEST_TEMPLATE/**', '.github/CODEOWNERS' ] + schedule: + - cron: '6 10 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: manual + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + java-version: '8' + distribution: 'temurin' + + - name: Build + run: ./gradlew build -x test --no-daemon + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" 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/math-check.yml b/.github/workflows/math-check.yml new file mode 100644 index 00000000000..a5db3351a94 --- /dev/null +++ b/.github/workflows/math-check.yml @@ -0,0 +1,93 @@ +name: Check Math Usage + +on: + push: + branches: [ 'master', 'release_**' ] + pull_request: + branches: [ 'develop', 'release_**' ] + workflow_dispatch: + +jobs: + check-math: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Check for java.lang.Math usage + id: check-math + shell: bash + run: | + echo "Checking for java.lang.Math usage..." + + touch math_usage.txt + + while IFS= read -r file; do + filename=$(basename "$file") + if [[ "$filename" == "StrictMathWrapper.java" || "$filename" == "MathWrapper.java" ]]; then + continue + fi + + perl -0777 -ne ' + s/"([^"\\]|\\.)*"//g; + s/'\''([^'\''\\]|\\.)*'\''//g; + s!/\*([^*]|\*[^/])*\*/!!g; + s!//[^\n]*!!g; + $hasMath = 0; + $hasMath = 1 if /^[\s]*import[\s]+java\.lang\.Math\b/m; + $hasMath = 1 if /\bjava\s*\.\s*lang\s*\.\s*Math\s*\./; + $hasMath = 1 if /(?> math_usage.txt + done < <(find . -type f -name "*.java") + + sort -u math_usage.txt -o math_usage.txt + + if [ -s math_usage.txt ]; then + echo "❌ Error: Forbidden Math usage found in the following files:" + cat math_usage.txt + echo "math_found=true" >> $GITHUB_OUTPUT + echo "Please use org.tron.common.math.StrictMathWrapper instead of direct Math usage." + else + echo "✅ No forbidden Math usage found" + echo "math_found=false" >> $GITHUB_OUTPUT + fi + + - name: Upload findings + if: steps.check-math.outputs.math_found == 'true' + uses: actions/upload-artifact@v6 + with: + name: math-usage-report + path: math_usage.txt + + - name: Create comment + if: github.event_name == 'pull_request' && steps.check-math.outputs.math_found == 'true' + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const findings = fs.readFileSync('math_usage.txt', 'utf8'); + const body = `### ❌ Math Usage Detection Results + + Found forbidden usage of \`java.lang.Math\` in the following files: + + \`\`\` + ${findings} + \`\`\` + + **Please review if this usage is intended.** + > [!CAUTION] + > Note: You should use \`org.tron.common.math.StrictMathWrapper\`. + > If you need to use \`java.lang.Math\`, please provide a justification. + `; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + + - name: Fail if Math usage found + if: steps.check-math.outputs.math_found == 'true' + run: exit 1 diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000000..f35538c0961 --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,515 @@ +name: PR Build + +on: + pull_request: + branches: [ 'master','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: + inputs: + job: + description: 'Job to run: all / macos / ubuntu / rockylinux / debian11' + required: false + default: 'all' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + + build-macos: + name: Build macos26 (JDK ${{ matrix.java }} / ${{ matrix.arch }}) + if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'macos' }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - java: '17' + runner: macos-26 + arch: aarch64 + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: macos26-${{ matrix.arch }}-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: macos26-${{ matrix.arch }}-gradle- + + - name: Build + run: ./gradlew clean build --no-daemon + + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + + build-ubuntu: + name: Build ubuntu24 (JDK 17 / aarch64) + if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'ubuntu' }} + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Check Java version + run: java -version + + - name: Cache Gradle packages + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ubuntu24-aarch64-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ubuntu24-aarch64-gradle- + + - name: Build + run: ./gradlew clean build --no-daemon + + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + + docker-build-rockylinux: + name: Build rockylinux (JDK 8 / x86_64) + if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'rockylinux' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + + container: + image: rockylinux:8 + + env: + GRADLE_USER_HOME: /github/home/.gradle + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + + steps: + - 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@v5 + with: + path: | + /github/home/.gradle/caches + /github/home/.gradle/wrapper + key: rockylinux-x86_64-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: | + rockylinux-x86_64-gradle- + + - name: Stop Gradle daemon + run: ./gradlew --stop || true + + - name: Build + run: ./gradlew clean build --no-daemon + + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + + - name: Test with RocksDB engine + run: ./gradlew :framework:testWithRocksDb --no-daemon + + docker-build-debian11: + name: Build debian11 (JDK 8 / x86_64) + if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'debian11' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + + container: + image: eclipse-temurin:8-jdk # base image is Debian 11 (Bullseye) + + defaults: + run: + shell: bash + + env: + GRADLE_USER_HOME: /github/home/.gradle + + steps: + - 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@v5 + with: + path: | + /github/home/.gradle/caches + /github/home/.gradle/wrapper + key: debian11-x86_64-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: | + debian11-x86_64-gradle- + + - name: Build + run: ./gradlew clean build --no-daemon --no-build-cache + + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + + - name: Test with RocksDB engine + run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache + + - name: Generate module coverage reports + run: ./gradlew jacocoTestReport --no-daemon + + - name: Upload PR coverage reports + uses: actions/upload-artifact@v6 + with: + name: jacoco-coverage-pr + path: | + **/build/reports/jacoco/test/jacocoTestReport.xml + if-no-files-found: error + + coverage-base: + name: Coverage Base (JDK 8 / x86_64) + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + container: + image: eclipse-temurin:8-jdk # base image is Debian 11 (Bullseye) + defaults: + run: + shell: bash + env: + GRADLE_USER_HOME: /github/home/.gradle + permissions: + contents: read + + steps: + - 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@v5 + with: + path: | + /github/home/.gradle/caches + /github/home/.gradle/wrapper + key: coverage-base-x86_64-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: | + 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) + run: ./gradlew jacocoTestReport --no-daemon + + - name: Upload base coverage reports + uses: actions/upload-artifact@v6 + with: + name: jacoco-coverage-base + path: | + **/build/reports/jacoco/test/jacocoTestReport.xml + if-no-files-found: warn + + coverage-gate: + name: Coverage Gate + needs: [docker-build-debian11, coverage-base] + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Download base coverage reports + uses: actions/download-artifact@v8 + with: + name: jacoco-coverage-base + path: coverage/base + + - name: Download PR coverage reports + uses: actions/download-artifact@v8 + with: + name: jacoco-coverage-pr + path: coverage/pr + + - name: Collect coverage report paths + id: collect-xml + run: | + BASE_XMLS=$(find coverage/base -name "jacocoTestReport.xml" | sort | paste -sd, -) + PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort | paste -sd, -) + if [ -z "$BASE_XMLS" ] || [ -z "$PR_XMLS" ]; then + echo "Missing jacocoTestReport.xml files for base or PR." + exit 1 + fi + echo "base_xmls=$BASE_XMLS" >> "$GITHUB_OUTPUT" + echo "pr_xmls=$PR_XMLS" >> "$GITHUB_OUTPUT" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Changed-line coverage (diff-cover) + id: diff-cover + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail + pip install --quiet 'diff-cover==9.2.0' + + # Ensure the base branch ref is available locally for diff-cover. + git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" + + PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort) + SRC_ROOTS=$(find . -type d -path '*/src/main/java' \ + -not -path './coverage/*' -not -path './.git/*' | sort) + if [ -z "$SRC_ROOTS" ]; then + echo "No src/main/java directories found; cannot run diff-cover." >&2 + exit 1 + fi + + set +e + diff-cover $PR_XMLS \ + --compare-branch="origin/${BASE_REF}" \ + --src-roots $SRC_ROOTS \ + --fail-under=0 \ + --json-report=diff-cover.json \ + --markdown-report=diff-cover.md + DIFF_RC=$? + set -e + + if [ ! -f diff-cover.json ]; then + echo "diff-cover did not produce JSON report (exit=${DIFF_RC})." >&2 + exit 1 + fi + + TOTAL_NUM_LINES=$(jq -r '.total_num_lines // 0' diff-cover.json) + if [ "${TOTAL_NUM_LINES}" = "0" ]; then + echo "No changed Java source lines; skipping changed-line gate." + echo "changed_line_coverage=NA" >> "$GITHUB_OUTPUT" + else + CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) + if [ -z "$CHANGED_LINE_COVERAGE" ]; then + echo "Unable to parse changed-line coverage from diff-cover.json." + exit 1 + fi + echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" + fi + + { + echo "### Changed-line Coverage (diff-cover)" + echo "" + if [ -f diff-cover.md ] && [ -s diff-cover.md ]; then + cat diff-cover.md + else + echo "_diff-cover produced no report._" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Aggregate base coverage + id: jacoco-base + uses: madrapps/jacoco-report@v1.7.2 + with: + paths: ${{ steps.collect-xml.outputs.base_xmls }} + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 0 + min-coverage-changed-files: 0 + skip-if-no-changes: true + comment-type: summary + title: '## Base Coverage Snapshot' + update-comment: false + + - name: Aggregate PR coverage + id: jacoco-pr + uses: madrapps/jacoco-report@v1.7.2 + with: + paths: ${{ steps.collect-xml.outputs.pr_xmls }} + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 0 + min-coverage-changed-files: 0 + skip-if-no-changes: true + comment-type: summary + title: '## PR Code Coverage Report' + update-comment: false + + - name: Enforce coverage gates + env: + BASE_OVERALL_RAW: ${{ steps.jacoco-base.outputs.coverage-overall }} + PR_OVERALL_RAW: ${{ steps.jacoco-pr.outputs.coverage-overall }} + CHANGED_LINE_RAW: ${{ steps.diff-cover.outputs.changed_line_coverage }} + run: | + set -euo pipefail + + MIN_CHANGED=60 + MAX_DROP=-0.1 + + sanitize() { + echo "$1" | tr -d ' %' + } + is_number() { + [[ "$1" =~ ^-?[0-9]+([.][0-9]+)?$ ]] + } + compare_float() { + # Usage: compare_float "" + # Example: compare_float "1.2 >= -0.1" + awk "BEGIN { if ($1) print 1; else print 0 }" + } + + # 1) Parse metrics from jacoco-report outputs + BASE_OVERALL="$(sanitize "$BASE_OVERALL_RAW")" + PR_OVERALL="$(sanitize "$PR_OVERALL_RAW")" + CHANGED_LINE="$(sanitize "$CHANGED_LINE_RAW")" + + if ! is_number "$BASE_OVERALL" || ! is_number "$PR_OVERALL"; then + echo "Failed to parse coverage values: base='${BASE_OVERALL}', pr='${PR_OVERALL}'." + exit 1 + fi + + # 2) Compare metrics against thresholds + DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }') + DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}") + + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_OK=1 + CHANGED_LINE_STATUS="SKIPPED (no changed Java source lines)" + elif [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then + echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'." + exit 1 + else + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" + else + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" + fi + fi + + # 3) Output base metrics (always visible in logs + step summary) + OVERALL_STATUS="PASS (>= ${MAX_DROP}%)" + if [ "$DELTA_OK" -ne 1 ]; then + OVERALL_STATUS="FAIL (< ${MAX_DROP}%)" + fi + + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_DISPLAY="NA" + else + CHANGED_LINE_DISPLAY="${CHANGED_LINE}%" + fi + + METRICS_TEXT=$(cat <> "$GITHUB_STEP_SUMMARY" + + # 4) Decide CI pass/fail + if [ "$DELTA_OK" -ne 1 ]; then + echo "Coverage gate failed: overall coverage dropped more than 0.1%." + echo "base=${BASE_OVERALL}% pr=${PR_OVERALL}% delta=${DELTA}%" + exit 1 + fi + + if [ "$CHANGED_LINE_OK" -ne 1 ]; then + echo "Coverage gate failed: changed-line coverage must be > 60%." + echo "changed-line=${CHANGED_LINE}%" + exit 1 + fi + + echo "Coverage gates passed." diff --git a/.github/workflows/pr-cancel.yml b/.github/workflows/pr-cancel.yml new file mode 100644 index 00000000000..3213026d3f9 --- /dev/null +++ b/.github/workflows/pr-cancel.yml @@ -0,0 +1,67 @@ +name: Cancel PR Workflows on Close + +on: + pull_request: + types: [ closed ] + +permissions: + actions: write + +jobs: + cancel: + name: Cancel In-Progress Workflows + if: github.event.pull_request.merged == false + runs-on: ubuntu-latest + steps: + - name: Cancel PR Build and Integration Tests + uses: actions/github-script@v8 + with: + script: | + 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) { + // 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, + 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 new file mode 100644 index 00000000000..506a823a4f7 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,153 @@ +name: PR Check + +on: + push: + branches: [ 'master', 'release_**' ] + pull_request: + branches: [ 'develop', 'release_**' ] + types: [ opened, edited, synchronize, reopened ] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + pr-lint: + name: PR Lint + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Validate PR title and description + uses: actions/github-script@v8 + with: + script: | + const title = context.payload.pull_request.title; + const body = context.payload.pull_request.body; + const errors = []; + const warnings = []; + + const allowedTypes = ['feat','fix','refactor','docs','style','test','chore','ci','perf','build','revert']; + const knownScopes = [ + 'framework','chainbase','actuator','consensus','common','crypto','plugins','protocol', + 'net','db','vm','tvm','api','jsonrpc','rpc','http','event','config', + 'block','proposal','trie','log','metrics','test','docker','version', + 'freezeV2','DynamicEnergy','stable-coin','reward','lite','toolkit' + ]; + + // 1. Title length check + if (!title || title.trim().length < 10) { + errors.push('PR title is too short (minimum 10 characters).'); + } + if (title && title.length > 72) { + errors.push(`PR title is too long (${title.length}/72 characters).`); + } + + // 2. Conventional format check + const conventionalRegex = /^(feat|fix|refactor|docs|style|test|chore|ci|perf|build|revert)(\([^)]+\))?:\s\S.*/; + if (title && !conventionalRegex.test(title)) { + errors.push( + 'PR title must follow conventional format: `type(scope): description`\n' + + ' Allowed types: ' + allowedTypes.map(t => `\`${t}\``).join(', ') + '\n' + + ' Example: `feat(tvm): add blob opcodes`' + ); + } + + // 3. No trailing period + if (title && title.endsWith('.')) { + errors.push('PR title should not end with a period (.).'); + } + + // 4. Description part should not start with a capital letter + if (title) { + const descMatch = title.match(/^\w+(?:\([^)]+\))?:\s*(.+)/); + if (descMatch) { + const desc = descMatch[1]; + if (/^[A-Z]/.test(desc)) { + errors.push('Description should not start with a capital letter.'); + } + } + } + + // 5. Scope validation (warning only) + if (title) { + const scopeMatch = title.match(/^\w+\(([^)]+)\):/); + if (scopeMatch && !knownScopes.includes(scopeMatch[1])) { + warnings.push(`Unknown scope \`${scopeMatch[1]}\`. See CONTRIBUTING.md for known scopes.`); + } + } + + // 6. PR description check + if (!body || body.trim().length < 20) { + errors.push('PR description is too short or empty (minimum 20 characters). Please describe what this PR does and why.'); + } + + // Output warnings + for (const w of warnings) { + core.warning(w); + } + + // Output result + if (errors.length > 0) { + const docLink = 'See [CONTRIBUTING.md](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/blob/develop/CONTRIBUTING.md#pull-request-guidelines) for details.'; + const message = '### PR Lint Failed\n\n' + errors.map(e => `- ${e}`).join('\n') + '\n\n' + docLink; + core.setFailed(message); + } else { + core.info('PR lint passed.'); + } + + checkstyle: + name: Checkstyle + runs-on: ubuntu-24.04-arm + + 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: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle- + + - name: Run Checkstyle + run: ./gradlew :framework:checkstyleMain :framework:checkstyleTest :plugins:checkstyleMain + + - name: Upload Checkstyle reports + if: failure() + uses: actions/upload-artifact@v6 + with: + name: checkstyle-reports + path: | + framework/build/reports/checkstyle/ + plugins/build/reports/checkstyle/ diff --git a/.github/workflows/pr-reviewer.yml b/.github/workflows/pr-reviewer.yml new file mode 100644 index 00000000000..bf124acf576 --- /dev/null +++ b/.github/workflows/pr-reviewer.yml @@ -0,0 +1,144 @@ +name: Auto Assign Reviewers + +on: + pull_request_target: + branches: [ 'develop', 'release_**' ] + types: [ opened, edited, reopened ] + +jobs: + assign-reviewers: + name: Assign Reviewers by Scope + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Assign reviewers based on PR title scope + uses: actions/github-script@v8 + with: + script: | + const title = context.payload.pull_request.title; + const prAuthor = context.payload.pull_request.user.login; + + // ── Scope → Reviewer mapping ────────────────────────────── + const scopeReviewers = { + 'framework': ['xxo1shine', '317787106'], + 'chainbase': ['halibobo1205', 'lvs0075'], + 'db': ['halibobo1205', 'xxo1shine'], + 'trie': ['halibobo1205', '317787106'], + 'actuator': ['yanghang8612', 'lxcmyf'], + 'consensus': ['lvs0075', 'xxo1shine'], + 'protocol': ['lvs0075', 'waynercheung'], + 'common': ['xxo1shine', 'lxcmyf'], + 'crypto': ['Federico2014', '3for'], + 'net': ['317787106', 'xxo1shine'], + 'vm': ['yanghang8612', 'CodeNinjaEvan'], + 'tvm': ['yanghang8612', 'CodeNinjaEvan'], + 'jsonrpc': ['0xbigapple', 'bladehan1'], + 'rpc': ['317787106', 'Sunny6889'], + 'http': ['Sunny6889', 'bladehan1'], + 'event': ['xxo1shine', '0xbigapple'], + 'config': ['317787106', 'halibobo1205'], + 'backup': ['xxo1shine', '317787106'], + 'lite': ['bladehan1', 'halibobo1205'], + 'toolkit': ['halibobo1205', 'Sunny6889'], + 'plugins': ['halibobo1205', 'Sunny6889'], + 'docker': ['3for', 'kuny0707'], + 'test': ['bladehan1', 'lxcmyf'], + 'metrics': ['halibobo1205', 'Sunny6889'], + 'api': ['0xbigapple', 'waynercheung', 'bladehan1'], + 'ci': ['bladehan1', 'halibobo1205'], + }; + const defaultReviewers = ['halibobo1205', '317787106']; + + // ── Normalize helper ───────────────────────────────────── + // Strip spaces, hyphens, underscores and lower-case so that + // "VM", " json rpc ", "chain-base", "Json_Rpc" all normalize + // to their canonical key form ("vm", "jsonrpc", "chainbase"). + const normalize = s => s.toLowerCase().replace(/[\s\-_]/g, ''); + + // ── Extract scope from conventional commit title ────────── + // Format: type(scope): description + // Also supports: type(scope1,scope2): description + const scopeMatch = title.match(/^\w+\(([^)]+)\):/); + const rawScope = scopeMatch ? scopeMatch[1] : null; + + core.info(`PR title : ${title}`); + core.info(`Raw scope: ${rawScope || '(none)'}`); + + // ── Skip if reviewers already assigned ────────────────── + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + const existing = pr.data.requested_reviewers || []; + if (existing.length > 0) { + core.info(`Reviewers already assigned (${existing.map(r => r.login).join(', ')}). Skipping.`); + return; + } + + // ── Determine reviewers ─────────────────────────────────── + // 1. Split by comma to support multi-scope: feat(vm,rpc): ... + // 2. Normalize each scope token + // 3. Match against keys: exact match first, then contains match + // (longest key wins to avoid "net" matching inside "jsonrpc") + let matched = new Set(); + let matchedScopes = []; + + if (rawScope) { + const tokens = rawScope.split(',').map(s => normalize(s.trim())); + // Pre-sort keys by length descending so longer keys match first + const sortedKeys = Object.keys(scopeReviewers) + .sort((a, b) => b.length - a.length); + + for (const token of tokens) { + if (!token) continue; + // Exact match + if (scopeReviewers[token]) { + matchedScopes.push(token); + scopeReviewers[token].forEach(r => matched.add(r)); + continue; + } + // Contains match: token contains a key, or key contains token + // Prefer longest key that matches + const found = sortedKeys.find(k => token.includes(k) || k.includes(token)); + if (found) { + matchedScopes.push(`${token}→${found}`); + scopeReviewers[found].forEach(r => matched.add(r)); + } + } + } + + let reviewers = matched.size > 0 + ? [...matched] + : defaultReviewers; + + core.info(`Matched scopes: ${matchedScopes.length > 0 ? matchedScopes.join(', ') : '(none — using default)'}`); + core.info(`Candidate reviewers: ${reviewers.join(', ')}`); + + // Exclude the PR author from the reviewer list + reviewers = reviewers.filter(r => r.toLowerCase() !== prAuthor.toLowerCase()); + + if (reviewers.length === 0) { + core.info('No eligible reviewers after excluding PR author. Skipping.'); + return; + } + + core.info(`Assigning reviewers: ${reviewers.join(', ')}`); + + // ── Request reviews ─────────────────────────────────────── + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + reviewers: reviewers, + }); + core.info('Reviewers assigned successfully.'); + } catch (error) { + // If a reviewer is not a collaborator the API returns 422; + // log the error but do not fail the workflow. + core.warning(`Failed to assign some reviewers: ${error.message}`); + } diff --git a/.gitignore b/.gitignore index b980800f353..3917bb44679 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ shareddata.* # protobuf generated classes src/main/gen - src/main/java/org/tron/core/bftconsensus src/test/java/org/tron/consensus2 src/main/java/META-INF/ @@ -55,3 +54,6 @@ Wallet # vm_trace /vm_trace/ + +/framework/propPath +.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fc2d449d532..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -sudo: required -language: java -jdk: oraclejdk8 -addons: - ssh_known_hosts: - - 47.93.9.236:22008 - sonarcloud: - organization: tron-zhaohong - token: - secure: "KXWEeQ1elAoQ0XfR54TQWfhrIdDP0+2CYPv2X9aWpU/YDl7hqZBAjcCOAUGvlbyM54jtG6YMaWwG48jlrOwwl5l/VjWSnUXF+7ixQy5ki0Ci9s7Y1pTQwV9SpL8TLIK2TYqabN8Mw+FJULASXLjYr9GerbbcUbCPTmcL6mQKw6ivxxpNPmz4eNoKAEuOzruO9fTXGAV3yr8Nn85c+mVxV8EUhkR17zpE9O8fvzOtSnYArWNOSCgDBn0EG45UNNPF2vn44s1c3h3gX1m3WK6PeCXPgy3hPqRn3wTG+xglnbqthGpo2wt1rJ83af+BwdYwvPEcUq84yLgXcE/aMkTKcVAfPWBP/6vblaoI90jxCeFji+MdMimKZAqIXt7oLqDZVmIq65de5YC5s7QTSbzJNY/tsAu3dqzSfbUJY6CRNFDcoenVpvgQkqb37TkDah4mJM8EUjbu2A9Q2HSFbyCVsQJtdasuu9cBOf6azK3U0XgFNBc0y2aziZrTnX30q7bi+5L/mbTnRdXrDqBOqyPeGtT77UZfcajHHmEWU/e6gYWiA/c+K25n13DD53Au6gpnnQ6ALeUl/1gDwz3fPBebJ5bVWrkIcLj7bbysjzfOvQmDS6G13RNz58Hm0/B7bVtZTr1E1q6Z1zEJwbuJYEJASNcezAfK5x/hIfZTGNqT3M8=" -cache: - directories: - - '$HOME/.sonar/cache' -script: -- sh tron.sh -- "./gradlew build" - -skip_build: -- README.md: -- LICENSE -deploy: - provider: script - script: bash deploy.sh - on: - branch: develop - - - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28f89f978ec..ef67a81e3ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,43 +1,348 @@ -# Contributing to java-tron - -java-tron is an open source project. - -It is the work of contributors. We appreciate your help! - -Here are instructions to get you started. They are not perfect, so -please let us know if anything feels wrong or incomplete. - -## Contribution guidelines - -### Pull requests - -First of all, java-tron follows [gitflow workflow]( -https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). -Please open pull requests to the **develop** branch. Once approved, -we will close the pull request and merge into master branch. - -We are always happy to receive pull requests, and do our best to -review them as fast as possible. Not sure if that typo is worth a pull -request? Do it! We would appreciate it. - -If your pull request is not accepted on the first try, don't be -discouraged as it can be a possible oversight. Please explain your code as -detailed as possible to make it easier for us to understand. - -### Create issues - -Any significant improvement should be documented as [a GitHub -issue](https://github.com/tronprotocol/java-tron/issues) before anyone -starts working on it. - -When filing an issue, make sure to answer these three questions: - -- What did you do? -- What did you expect to see? -- What did you see instead? - -### Please check existing issues and docs first! - -Please take a moment to check that your bug report or improvement proposal -doesn't already exist. If it does, please add a quick "+1" or "I have this problem too". -This will help prioritize the most common problems and requests. +# Contributing to java-tron + +java-tron is an open-source project which needs the support of open-source contributors. + +Below are the instructions. We understand that there is much left to be desired, and if you see any room for improvement, please let us know. Thank you. + +Here are some guidelines to get started quickly and easily: +- [Reporting An Issue](#Reporting-An-Issue) +- [Working on java-tron](#Working-on-java-tron) + - [Key Branches](#Key-Branches) + - [Submitting Code](#Submitting-Code) +- [Code Review Guidelines](#Code-Review-Guidelines) + - [Terminology](#Terminology) + - [The Process](#The-Process) + - [Code Style](#Code-Style) + - [Commit Messages](#Commit-Messages) + - [Branch Naming Conventions](#Branch-Naming-Conventions) + - [Pull Request Guidelines](#Pull-Request-Guidelines) + - [PR Title Format](#PR-Title-Format) + - [Type and Scope Reference](#Type-and-Scope-Reference) + - [PR Description](#PR-Description) + - [Special Situations And How To Deal With Them](#Special-Situations-And-How-To-Deal-With-Them) +- [Conduct](#Conduct) + + +## Reporting An Issue + +If you have any question about java-tron, please search [existing issues](https://github.com/tronprotocol/java-tron/issues?q=is%3Aissue%20state%3Aclosed%20OR%20state%3Aopen) first to avoid duplicates. Your questions might already be under discussion or part of our roadmap. Checking first helps us streamline efforts and focus on new contributions. + +### Ask a question +Feel free to ask any java-tron related question to solve your doubt. Please click **Ask a question** in GitHub Issues, using [Ask a question](.github/ISSUE_TEMPLATE/ask-a-question.md) template. + +### Report a bug + +If you think you've found a bug with java-tron, please click **Report a bug** in GitHub Issues, using [Report a bug](.github/ISSUE_TEMPLATE/report-a-bug.md) template. + +### Request a feature + +If you have any good feature suggestions for java-tron, please click **Request a feature** in GitHub Issues, using [Request a feature](.github/ISSUE_TEMPLATE/request-a-feature.md) template. + + +## Working on java-tron +Thank you for considering to help out with the source code! We welcome contributions from anyone on the internet, and are grateful for even the smallest of fixes! + +If you’d like to contribute to java-tron, for small fixes, we recommend that you send a pull request (PR) for the maintainers to review and merge into the main code base, make sure the PR contains a detailed description. For more complex changes, you need to submit an issue to the TIP repository to detail your motive and implementation plan, etc. For how to submit a TIP issue, please refer to [TIP Specification](https://github.com/tronprotocol/tips#to-submit-a-tip). + + +As the author of TIP issue, you are expected to encourage developers to discuss this issue, flesh out your issue by collecting their feedback, and eventually put your issue into practice. + + +### Key Branches +java-tron only has `master`, `develop`, `release-*`, `feature-*`, and `hotfix-*` branches, which are described below: + +- ``develop`` branch + The `develop` branch only accept merge request from other forked branches or`release_*` branches. It is not allowed to directly push changes to the `develop` branch. A `release_*` branch has to be pulled from the develop branch when a new build is to be released. + +- ``master`` branch + `release_*` branches and `hotfix/*` branches should only be merged into the `master` branch when a new build is released. + +- ``release`` branch + `release_*` is a branch pulled from the `develop` branch for release. It should be merged into `master` after a regression test and will be permanently kept in the repository. If a bug is identified in a `release_*` branch, its fixes should be directly merged into the branch. After passing the regression test, the `release_*` branch should be merged back into the `develop` branch. Essentially, a `release_*` branch serves as a snapshot for each release. + +- ``feature`` branch + `feature/*` is an important feature branch pulled from the `develop` branch. After the `feature/*` branch is code-complete, it should be merged back to the `develop` branch. The `feature/*` branch is maintainable. + +- ``hotfix`` branch + It is pulled from the `master` branch and should be merged back into the master branch and the `develop` branch. Only pull requests of the fork repository (pull requests for bug fixes) should be merged into the `hotfix/` branch. `hotfix/` branches are used only for fixing bugs found after release. + + +### Submitting Code + +If you want to contribute code to java-tron, please follow the following steps. + +* Fork the Repository + + Visit [tronprotocol/java-tron](https://github.com/tronprotocol/java-tron/) and click **Fork** to create a fork repository under your GitHub account. + +* Setup Local Environment + + Clone your fork repository to local and add the official repository as **upstream**. + ``` + git clone https://github.com/yourname/java-tron.git + + cd java-tron + + git remote add upstream https://github.com/tronprotocol/java-tron.git + ``` + +* Synchronize and Develop + + Before developing new features, please synchronize your local `develop` branch with the upstream repository and update to your fork repository. + ``` + git fetch upstream + git checkout develop + # `--no-ff` means to turn off the default fast merge mode + git merge upstream/develop --no-ff + git push origin develop + ``` + + Create a new branch for development. Please refer to [Branch Naming Conventions](#Branch-Naming-Conventions). + ``` + git checkout -b feature/branch_name develop + ``` + +* Commit and Push + + Write and commit the new code when it is completed. Please refer to [Commit Messages](#Commit-Messages). + ``` + git add . + git commit -m 'commit message' + ``` + + Push the new branch to your fork repository + ``` + git push origin feature/branch_name + ``` + +* Submit a pull request + + Submit a pull request (PR) from your fork repository to `tronprotocol/java-tron`. + Please be sure to click on the link in the red box shown below. Select the base branch for `tronprotocol/java-tron` and the compare branch for your fork repository. + ![image](https://raw.githubusercontent.com/tronprotocol/documentation-en/master/images/javatron_pr.png) + + + +## Code Review Guidelines +The only way to get code into java-tron is to send a pull request. Those pull requests need to be reviewed by someone. The following guide explains our expectations around PRs for both authors and reviewers. + +### Terminology +- The author of a pull request is the entity who wrote the diff and submitted it to GitHub. +- The team consists of people with commit rights on the java-tron repository. +- The reviewer is the person assigned to review the diff. The reviewer must be a team member. +- The code owner is the person responsible for the subsystem being modified by the PR. + +### The Process +The first decision to make for any PR is whether it’s worth including at all. This decision lies primarily with the code owner, but may be negotiated with team members. + +To make the decision we must understand what the PR is about. If there isn’t enough description content or the diff is too large, request an explanation. Anyone can do this part. + +We expect that reviewers check the style and functionality of the PR, providing comments to the author using the GitHub review system. Reviewers should follow up with the PR until it is in good shape, then approve the PR. Approved PRs can be merged by any code owner. + +When communicating with authors, be polite and respectful. + +### Code Style +We would like all developers to follow a standard development flow and coding style. Therefore, we suggest the following: +1. Review the code with coding style checkers. +2. Review the code before submission. +3. Run standardized tests. + +`Sonar`-scanner and CI checks (GitHub Actions) will be automatically triggered when a pull request has been submitted. When a PR passes all the checks, the **java-tron** maintainers will then review the PR and offer feedback and modifications when necessary. Once adopted, the PR will be closed and merged into the `develop` branch. + +We are glad to receive your pull requests and will try our best to review them as soon as we can. Any pull request is welcome, even if it is for a typo. + +Please kindly address the issue you find. We would appreciate your contribution. + +Please do not be discouraged if your pull request is not accepted, as it may be an oversight. Please explain your code as detailed as possible to make it easier to understand. + +Please make sure your submission meets the following code style: + +- The code must conform to [Google Code Style](https://google.github.io/styleguide/javaguide.html). +- The code must have passed the Sonar scanner test. +- The code has to be pulled from the `develop` branch. +- The commit message should start with a verb, whose initial should not be capitalized. +- The commit message title should be between 10 and 72 characters in length. + + + +### Commit Messages + +Commit messages should follow the rule below, we provide a template with corresponding instructions. + +Template: +``` +(): + + + +