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/pr-check.yml b/.github/workflows/pr-check.yml index 19425209bbc..7ae169a8690 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -103,6 +103,22 @@ 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: Set up JDK 17 uses: actions/setup-java@v5 with: diff --git a/README.md b/README.md index 575409b3a96..be84b44150b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ The TRON network is mainly divided into: - **Private Networks** Customized TRON networks set up by private entities for testing, development, or specific use cases. -Network selection is performed by specifying the appropriate configuration file upon full-node startup. Mainnet configuration: [config.conf](framework/src/main/resources/config.conf); Nile testnet configuration: [config-nile.conf](https://github.com/tron-nile-testnet/nile-testnet/blob/master/framework/src/main/resources/config-nile.conf) +Network selection is performed by specifying the appropriate configuration file upon full-node startup. Built-in configuration template: [reference.conf](common/src/main/resources/reference.conf); Mainnet configuration: [config.conf](framework/src/main/resources/config.conf); Nile testnet configuration: [config-nile.conf](https://github.com/tron-nile-testnet/nile-testnet/blob/master/framework/src/main/resources/config-nile.conf) ### 1. Join the TRON main network Launch a main-network full node with the built-in default configuration: diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 1ac96b9d59d..0dc8fb31ada 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -705,6 +705,9 @@ public Pair execute(byte[] data) { // 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/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 80d972041dc..3ed968e1afa 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,6 +1616,10 @@ 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) { stackPushZero(); 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..63acf64b64f 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; @@ -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..8724a688548 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; @@ -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(); diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 1b46f3fa8db..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,38 +0,0 @@ -# DEPRECATED: Codecov integration is no longer active. -# Coverage is now handled by JaCoCo + madrapps/jacoco-report in pr-build.yml. -# This file is retained for reference only and can be safely deleted. - -# Post a Codecov comment on pull requests. If don't need comment, use comment: false, else use following -comment: false -#comment: -# # Show coverage diff, flags table, and changed files in the PR comment -# layout: "diff, flags, files" -# # Update existing comment if present, otherwise create a new one -# behavior: default -# # Post a comment even when coverage numbers do not change -# require_changes: false -# # Do not require a base report before posting the comment -# require_base: false -# # Require the PR head commit to have a coverage report -# require_head: true -# # Show both project coverage and patch coverage in the PR comment -# hide_project_coverage: false - -codecov: - # Do not wait for all CI checks to pass before sending notifications - require_ci_to_pass: false - notify: - wait_for_ci: false - -coverage: - status: - project: # PR coverage/project UI - default: - # Compare against the base branch automatically - target: auto - # Allow a small coverage drop tolerance - threshold: 0.02% -# patch: off - patch: # PR coverage/patch UI - default: - target: 60% diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 3fe1e878ffb..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..3a2c59d5139 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 = 100; + public static final int MAX_TOKEN_COUNT = 100_000; + } 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/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..8b69f6ef917 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -25,19 +25,6 @@ # 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". -# # ============================================================================= net { @@ -46,47 +33,70 @@ net { } storage { - # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) + # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB), case-insensitive db.engine = "LEVELDB" + + # Controls the database write strategy. + # true - Synchronous writes. Higher durability, lower performance. + # false - Asynchronous writes. Higher performance, but recent writes may be + # lost if the machine crashes before data is flushed to disk. + # Asynchronous writes can significantly improve FullNode block sync performance. db.sync = false - db.directory = "database" - # Index directory (legacy, not consumed by any runtime code, kept for CLI/test compatibility) - index.directory = "index" - index.switch = "on" + db.directory = "database" # Whether to write transaction result in transactionRetStore transHistory.switch = "on" - # Per-database LevelDB option overrides. Default: empty (all databases use global defaults). - # setting can improve leveldb performance .... start, deprecated for arm - # node: if this will increase process fds, you may check your ulimit if 'too many open files' error occurs - # see https://github.com/tronprotocol/tips/blob/master/tip-343.md for detail - # if you find block sync has lower performance, you can try this settings + # Per-database LevelDB option overrides.Default: empty. All databases use global LevelDB settings. + # These settings can be tuned to improve database performance during block synchronization. + # Note: + # - Increasing `maxOpenFiles` may significantly increase file descriptor usage. + # - If "Too many open files" errors occur, check the system `ulimit` configuration. + # - See TIP-343 for tuning recommendations: + # https://github.com/tronprotocol/tips/blob/master/tip-343.md + # The following presets are provided as default. If block synchronization + # performance is unsatisfactory, consider adjusting the settings accordingly. + # + # Global default settings: # default = { + # blockSize = 4096, // 4 KB + # writeBufferSize = 16777216, // 16 MB + # cacheSize = 33554432, // 32 MB # maxOpenFiles = 100 # } + # Default for bulk-read databases: code, contract # defaultM = { - # maxOpenFiles = 500 + # blockSize = 4096, // 4 KB + # writeBufferSize = 67108864, // 64 MB + # cacheSize = 33554432, // 32 MB + # maxOpenFiles = 100 // recommend 500 for production # } + # Default for frequently accessed databases: account, delegation, storage-row # defaultL = { - # maxOpenFiles = 1000 + # blockSize = 4096, // 4 KB + # writeBufferSize = 67108864, // 64 MB + # cacheSize = 33554432, // 32 MB + # maxOpenFiles = 100 // recommend 1000 for production # } - # setting can improve leveldb performance .... end, deprecated for arm - # 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 @@ -140,12 +150,11 @@ node.discovery = { # } 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 members = [ - # "ip", - # "ip" + # "ip", # Peer IP list, better to add at most one IP; must not contain this node's own IP ] } @@ -166,8 +175,6 @@ node.metrics = { node { # Trust node for solidity node (example: "127.0.0.1:50051"). - # Empty string here = "not configured"; Args.java bridge converts "" → null so the - # runtime behavior matches develop (trustNodeAddr is null unless user sets the key). trustNode = "" # Expose extension api to public or not @@ -192,7 +199,12 @@ node { maxHttpConnectNumber = 50 minParticipationRate = 0 - # Whether to enable shielded transaction API + # 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 @@ -211,8 +223,7 @@ node { 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`. @@ -247,10 +258,9 @@ node { PBFTEnable = true PBFTPort = 8092 - # Maximum HTTP request body size, default 4MB. Independent from rpc.maxMessageSize. - maxMessageSize = 4M - maxNestingDepth = 100 - maxTokenCount = 100000 + # Maximum HTTP request body size (default 4M). Setting to 0 rejects all non-empty request bodies. + # Independent from rpc.maxMessageSize. + maxMessageSize = 4194304 } rpc { @@ -265,19 +275,20 @@ node { 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 @@ -330,7 +341,6 @@ node { blockCacheTimeout = 60 # TCP and transaction limits - receiveTcpMinDataLength = 2048 maxTransactionPendingSize = 2000 pendingTransactionTimeout = 60000 # total cached trx across handler queues + pending + rePush @@ -347,28 +357,50 @@ node { validContractProto.threads = 0 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 + 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 @@ -390,8 +422,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 +435,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 blocking 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 +465,7 @@ rate.limiter = { # } ] + # Per-method gRPC rate limits. component is "package.ServiceName/MethodName". rpc = [ # { # component = "protocol.Wallet/GetBlockByLatestNum2", @@ -443,14 +485,21 @@ rate.limiter = { ] 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 } seed.node = { @@ -490,7 +539,21 @@ 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" @@ -512,6 +575,12 @@ 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, @@ -650,12 +719,20 @@ 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 = localwitness = [ @@ -701,6 +778,18 @@ 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). @@ -758,34 +847,38 @@ event.subscribe = { enable = false 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 + # 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 topics = [ { - triggerName = "block" + triggerName = "block" // block trigger, the value can't be modified enable = false - topic = "block" - solidified = false + 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 - ethCompatible = false + ethCompatible = false // if set true, add transactionIndex, cumulativeEnergyUsed, preCumulativeLogCount, logList, energyUnitPrice. Default: false }, { - triggerName = "contractevent" + triggerName = "contractevent" // contractevent represents contractlog data decoded by the ABI. enable = false topic = "contractevent" }, @@ -793,11 +886,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,18 +902,18 @@ event.subscribe = { triggerName = "soliditylog" enable = false topic = "soliditylog" - redundancy = false + redundancy = false // if set true, solidityevent will also be regarded as soliditylog } ] 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 contractAddress = [ - "" + "" // contract address to subscribe; "" means any contract address ] contractTopic = [ - "" + "" // contract topic to subscribe; "" means any contract topic ] } } diff --git a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java index 962b6a349ab..559198100fb 100644 --- a/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java @@ -20,8 +20,8 @@ private static Config withRef() { public void testDefaults() { CommitteeConfig cc = CommitteeConfig.fromConfig(withRef()); assertEquals(0, cc.getAllowCreationOfContracts()); - assertEquals(0, cc.getAllowPBFT()); - assertEquals(20, cc.getPBFTExpireNum()); + assertEquals(0, cc.getAllowPbft()); + assertEquals(20, cc.getPbftExpireNum()); assertEquals(0, cc.getUnfreezeDelayDays()); assertEquals(0, cc.getAllowDynamicEnergy()); } @@ -32,8 +32,8 @@ public void testFromConfig() { "committee { allowCreationOfContracts = 1, allowPBFT = 1, pBFTExpireNum = 30 }"); CommitteeConfig cc = CommitteeConfig.fromConfig(config); assertEquals(1, cc.getAllowCreationOfContracts()); - assertEquals(1, cc.getAllowPBFT()); - assertEquals(30, cc.getPBFTExpireNum()); + assertEquals(1, cc.getAllowPbft()); + assertEquals(30, cc.getPbftExpireNum()); } @Test diff --git a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java index 361d9f48581..ca0cbefaddd 100644 --- a/common/src/test/java/org/tron/core/config/args/EventConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -79,4 +79,11 @@ public void testFilter() { assertEquals(2, ec.getFilter().getContractAddress().size()); assertEquals(1, ec.getFilter().getContractTopic().size()); } + + @Test + public void testTopicsEmptyList() { + EventConfig ec = EventConfig.fromConfig(withRef( + "event.subscribe.topics = []")); + assertTrue(ec.getTopics().isEmpty()); + } } diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index d4fbc05e730..bbc2d2475ee 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -245,8 +245,6 @@ public void testValidContractProtoThreadsExplicitPreserved() { @Test public void testTrustNodeNotDefaultedByReferenceConf() { - // reference.conf intentionally omits `node.trustNode` so that empty configs - // preserve develop's behavior (trustNodeAddr stays null in the Args bridge). NodeConfig nc = NodeConfig.fromConfig(withRef()); assertTrue(nc.getTrustNode() == null || nc.getTrustNode().isEmpty()); } diff --git a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java index 7b4d8a87d45..c3b827a8ba4 100644 --- a/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java @@ -1,6 +1,7 @@ package org.tron.core.config.args; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.typesafe.config.Config; @@ -29,6 +30,7 @@ public void testDefaults() { assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001); assertTrue(rl.getHttp().isEmpty()); assertTrue(rl.getRpc().isEmpty()); + assertFalse(rl.isApiNonBlocking()); } @Test @@ -40,7 +42,8 @@ public void testFromConfig() { + " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter," + " paramString = \"qps=10\" }]," + " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter," - + " paramString = \"permit=1\" }]" + + " paramString = \"permit=1\" }]," + + " apiNonBlocking = true" + "}"); RateLimiterConfig rl = RateLimiterConfig.fromConfig(config); assertEquals(100, rl.getGlobal().getQps()); @@ -50,5 +53,6 @@ public void testFromConfig() { assertEquals("TestServlet", rl.getHttp().get(0).getComponent()); assertEquals(1, rl.getRpc().size()); assertEquals("TestRpc", rl.getRpc().get(0).getComponent()); + assertTrue(rl.isApiNonBlocking()); } } diff --git a/common/src/test/java/org/tron/core/config/args/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/logsfilter/EventPluginLoader.java b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java index 7061b2e9d57..c0b7afd6779 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java +++ b/framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java @@ -3,6 +3,7 @@ import com.beust.jcommander.internal.Sets; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; import java.io.File; import java.util.HashSet; import java.util.List; @@ -14,8 +15,10 @@ import org.bouncycastle.util.encoders.Hex; import org.pf4j.CompoundPluginDescriptorFinder; import org.pf4j.DefaultPluginManager; +import org.pf4j.DefaultVersionManager; import org.pf4j.ManifestPluginDescriptorFinder; import org.pf4j.PluginManager; +import org.pf4j.VersionManager; import org.springframework.util.StringUtils; import org.tron.common.logsfilter.nativequeue.NativeMessageQueue; import org.tron.common.logsfilter.trigger.BlockLogTrigger; @@ -29,6 +32,16 @@ @Slf4j public class EventPluginLoader { + /** + * Minimum event-plugin Plugin-Version compatible with this node. Bumped to 3.0.0 to + * reject pre-fastjson-removal builds whose worker threads would fail with + * NoClassDefFoundError on com.alibaba.fastjson at runtime. The previous event-plugin + * release is 2.2.0, so 3.0.0 is the first version that ships the Jackson replacement. + */ + static final String MIN_PLUGIN_VERSION = "3.0.0"; + + private static final VersionManager VERSION_MANAGER = new DefaultVersionManager(); + private static EventPluginLoader instance; private long MAX_PENDING_SIZE = 50000; @@ -457,6 +470,10 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return false; } + if (!isPluginVersionSupported(pluginManager, pluginId)) { + return false; + } + pluginManager.startPlugins(); eventListeners = pluginManager.getExtensions(IPluginEventListener.class); @@ -471,6 +488,21 @@ protected CompoundPluginDescriptorFinder createPluginDescriptorFinder() { return true; } + static boolean isPluginVersionSupported(PluginManager pm, String pluginId) { + String pluginVersion = pm.getPlugin(pluginId).getDescriptor().getVersion(); + if (Strings.isNullOrEmpty(pluginVersion)) { + return false; + } + boolean isSupported = VERSION_MANAGER.compareVersions(pluginVersion, MIN_PLUGIN_VERSION) >= 0; + + if (!isSupported) { + logger.error( + "event-plugin '{}' version {} is older than required {}, please upgrade event-plugin", + pluginId, pluginVersion, MIN_PLUGIN_VERSION); + } + return isSupported; + } + public void stopPlugin() { if (Objects.nonNull(pluginManager)) { pluginManager.stopPlugins(); diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0482643d8d0..b705b26edc2 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -270,6 +270,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 +507,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 +568,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) @@ -3672,9 +3684,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 +3809,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 +3846,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 +3919,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 +4013,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 +4053,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 +4122,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 +4323,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..d2aa42dfcea 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1140,6 +1140,11 @@ 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"); + } applyBlock(item.getBlk().setSwitch(true)); tmpSession.commit(); } catch (AccountResourceInsufficientException @@ -1230,7 +1235,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()); @@ -1529,6 +1534,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 +1633,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 +1645,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<>(); 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..f703779c616 100644 --- a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java +++ b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java @@ -272,7 +272,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/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/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 3086cbb3619..f488c32df4c 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -107,9 +107,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) IRateLimiter rateLimiter = container.get(KEY_PREFIX_HTTP, getClass().getSimpleName()); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index 2093930ca98..a332757457f 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,6 +1,8 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -22,6 +24,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 +33,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 +110,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { rootNode = MAPPER.readTree(body); if (rootNode == null || rootNode.isMissingNode()) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); return; } } catch (JsonProcessingException e) { - writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "Parse error", null, false); + writeJsonRpcError(resp, JsonRpcError.PARSE_ERROR, "JSON parse error", null, false); + return; + } + + if (!rootNode.isObject() && !rootNode.isArray()) { + writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); return; } @@ -159,8 +177,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 +188,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 +246,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 +295,7 @@ private void writeJsonRpcError(HttpServletResponse resp, JsonRpcError error, Str } else { bytes = MAPPER.writeValueAsBytes(errorObj); } - resp.setContentType("application/json-rpc; charset=utf-8"); + resp.setContentType("application/json-rpc"); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentLength(bytes.length); resp.getOutputStream().write(bytes); diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java index 4b3043274d2..11c55e3a2c3 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/GlobalRateLimiter.java @@ -23,21 +23,43 @@ public class GlobalRateLimiter { public static boolean tryAcquire(RuntimeData runtimeData) { String ip = runtimeData.getRemoteAddr(); if (!Strings.isNullOrEmpty(ip)) { - RateLimiter r; - try { - // cache.get is atomic: only one loader executes per key under concurrent requests, - // preventing multiple RateLimiter instances from being created for the same IP. - r = cache.get(ip, () -> RateLimiter.create(IP_QPS)); - } catch (Exception e) { - logger.warn("Failed to load IP rate limiter for {}, denying request: {}", - ip, e.getMessage()); + RateLimiter r = loadIpLimiter(ip); + if (r == null || !r.tryAcquire()) { return false; } - if (!r.tryAcquire()) { + } + return rateLimiter.tryAcquire(); + } + + public static boolean acquire(RuntimeData runtimeData) { + String ip = runtimeData.getRemoteAddr(); + if (!Strings.isNullOrEmpty(ip)) { + RateLimiter r = loadIpLimiter(ip); + if (r == null) { return false; } + r.acquire(); + } + rateLimiter.acquire(); + return true; + } + + public static boolean acquirePermit(RuntimeData runtimeData) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(runtimeData) + : acquire(runtimeData); + } + + private static RateLimiter loadIpLimiter(String ip) { + try { + // cache.get is atomic: only one loader executes per key under concurrent requests, + // preventing multiple RateLimiter instances from being created for the same IP. + return cache.get(ip, () -> RateLimiter.create(IP_QPS)); + } catch (Exception e) { + logger.warn("Failed to load IP rate limiter for {}, denying request: {}", + ip, e.getMessage()); + return null; } - return rateLimiter.tryAcquire(); } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java index a07cf955828..85e94f2e768 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/RateLimiterInterceptor.java @@ -108,9 +108,10 @@ public Listener interceptCall(ServerCall call, RuntimeData runtimeData = new RuntimeData(call); // Check per-endpoint first to avoid consuming global IP/QPS quota for requests - // that would be rejected by the per-endpoint limiter anyway. - boolean perEndpointAcquired = rateLimiter == null || rateLimiter.tryAcquire(runtimeData); - boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.tryAcquire(runtimeData); + // that would be rejected by the per-endpoint limiter anyway. acquirePermit() + // chooses blocking or non-blocking semantics based on rate.limiter.apiNonBlocking. + boolean perEndpointAcquired = rateLimiter == null || rateLimiter.acquirePermit(runtimeData); + boolean acquireResource = perEndpointAcquired && GlobalRateLimiter.acquirePermit(runtimeData); if (!acquireResource) { // Release the per-endpoint permit when global rejected, to avoid semaphore leak. diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java index 8f5b5a487bf..63d4cc77587 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/DefaultBaseQqsAdapter.java @@ -15,4 +15,9 @@ public DefaultBaseQqsAdapter(String paramString) { public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java index 4adc142ed28..eb85baa8b41 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/GlobalPreemptibleAdapter.java @@ -21,4 +21,8 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java index c6fb089063a..0ebd21149a7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IPQPSRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(data.getRemoteAddr()); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(data.getRemoteAddr()); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java index 46ed8beee92..29f7b61b6a5 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/IRateLimiter.java @@ -1,9 +1,17 @@ package org.tron.core.services.ratelimiter.adapter; +import org.tron.core.config.args.Args; import org.tron.core.services.ratelimiter.RuntimeData; public interface IRateLimiter { boolean tryAcquire(RuntimeData data); + boolean acquire(RuntimeData data); + + default boolean acquirePermit(RuntimeData data) { + return Args.getInstance().isRateLimiterApiNonBlocking() + ? tryAcquire(data) + : acquire(data); + } } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java index 846a5eb1c4e..62074eac885 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/adapter/QpsRateLimiterAdapter.java @@ -16,4 +16,9 @@ public boolean tryAcquire(RuntimeData data) { return strategy.tryAcquire(); } + @Override + public boolean acquire(RuntimeData data) { + return strategy.acquire(); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java index 0a29183d762..e7b7f560b29 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/GlobalPreemptibleStrategy.java @@ -3,11 +3,15 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class GlobalPreemptibleStrategy extends Strategy { public static final String STRATEGY_PARAM_PERMIT = "permit"; public static final int DEFAULT_PERMIT_NUM = 1; + public static final int DEFAULT_ACQUIRE_TIMEOUT = 2; private Semaphore sp; public GlobalPreemptibleStrategy(String paramString) { @@ -23,15 +27,25 @@ protected Map defaultParam() { return map; } - // Non-blocking: immediately rejects if no permit is available. - // Intentional change from the previous tryAcquire(2, TimeUnit.SECONDS) behaviour: - // blocking the caller for up to 2 s ties up Netty IO / gRPC executor threads and - // masks overload rather than shedding it. All rate-limiting in this stack is now - // non-blocking to keep the thread model consistent with GlobalRateLimiter. + // Non-blocking: immediately rejects if no permit is available. Used when the + // apiNonBlocking switch is on, to shed overload instead of tying up Netty IO / + // gRPC executor threads while waiting for a permit. public boolean tryAcquire() { return sp.tryAcquire(); } + public boolean acquire() { + try { + return sp.tryAcquire(DEFAULT_ACQUIRE_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Restore the interrupt flag and reject — caller must not release a permit + // it never acquired. + logger.error("acquire permit with error: {}", e.getMessage()); + Thread.currentThread().interrupt(); + return false; + } + } + public void release() { sp.release(); } diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java index 6589c90fe1d..7ffd1f04eb7 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/IPQpsStrategy.java @@ -22,17 +22,29 @@ public IPQpsStrategy(String paramString) { } public boolean tryAcquire(String ip) { - RateLimiter limiter; + RateLimiter limiter = loadLimiter(ip); + return limiter != null && limiter.tryAcquire(); + } + + public boolean acquire(String ip) { + RateLimiter limiter = loadLimiter(ip); + if (limiter == null) { + return false; + } + limiter.acquire(); + return true; + } + + private RateLimiter loadLimiter(String ip) { try { // cache.get is atomic: only one loader executes per key under concurrent requests, // preventing multiple RateLimiter instances from being created for the same IP. - limiter = ipLimiter.get(ip, this::newRateLimiter); + return ipLimiter.get(ip, this::newRateLimiter); } catch (Exception e) { logger.warn("Failed to load IP rate limiter for {}, denying request: {}", ip, e.getMessage()); - return false; + return null; } - return limiter.tryAcquire(); } private RateLimiter newRateLimiter() { diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java index 7e0466448b3..9116af1b7da 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/strategy/QpsStrategy.java @@ -29,4 +29,9 @@ protected Map defaultParam() { public boolean tryAcquire() { return rateLimiter.tryAcquire(); } + + public boolean acquire() { + rateLimiter.acquire(); + return true; + } } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/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/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/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/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/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/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/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/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..717d6c4cf64 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -23,6 +23,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; @@ -876,6 +877,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, 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/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/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..93b84450f7b 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,10 @@ package org.tron.core.net; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.lang.reflect.Method; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import org.junit.Assert; @@ -14,6 +16,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; @@ -223,4 +226,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/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/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java index 1ae341696eb..8cca558d151 100644 --- a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -167,14 +167,14 @@ public void testBuildsEachWhitelistedAdapter() { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); - // tryAcquire returned false — no permit was taken, nothing to release + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); + // acquirePermit returned false — no permit was taken, nothing to release verify(perEndpoint, never()).release(); } } @@ -186,13 +186,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() throws Exception @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws Exception { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -203,11 +203,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() throws E @Test public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); servlet.service(request, response); @@ -223,11 +223,11 @@ public void testGlobalRejectedReleasesPreemptiblePermit() throws Exception { @Test public void testBothPassPermitReleasedAfterRequest() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_HTTP, "TestServlet", perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); @@ -243,11 +243,11 @@ public void testBothPassPermitReleasedAfterRequest() throws Exception { public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { // No entry added to container — container.get() returns null try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); servlet.service(request, response); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcServletTest.java index fa45ca48876..c5e87384b99 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 @@ -25,6 +25,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 { @@ -245,6 +246,160 @@ public void normalRequest_commitsRpcServerResponse() throws Exception { assertArrayEquals(rpcResp, resp.getContentAsByteArray()); } + // --- Content-Type header: must be application/json-rpc (no charset suffix) --- + + @Test + public void errorResponse_contentTypeIsApplicationJsonRpc() throws Exception { + MockHttpServletResponse resp = doPost("not valid json"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void batchResponse_contentTypeIsApplicationJsonRpc() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}]"); + assertEquals("application/json-rpc", resp.getContentType()); + } + + @Test + public void allNotificationBatch_contentTypeIsApplicationJsonRpc() throws Exception { + // notification: rpcServer returns 0 bytes → empty batchResult → early return path + doAnswer(inv -> 0).when(mockRpcServer) + .handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"method\":\"eth_blockNumber\"}]"); + assertEquals(200, resp.getStatus()); + assertEquals(0, resp.getContentLength()); + assertEquals("application/json-rpc", resp.getContentType()); + } + + // --- Primitive root node → Invalid Request (-32600), id must be JSON null --- + + @Test + public void primitiveRootNull_returnsInvalidRequestWithJsonNullId() throws Exception { + MockHttpServletResponse resp = doPost("null"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertFalse(body.isArray()); + assertEquals("2.0", body.get("jsonrpc").asText()); + assertEquals(-32600, body.get("error").get("code").asInt()); + assertTrue("id must be JSON null, not the string \"null\"", body.get("id").isNull()); + assertFalse("id must not be a string", body.get("id").isTextual()); + } + + @Test + public void primitiveRootBoolean_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("true"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootNumber_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("123"); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @Test + public void primitiveRootString_returnsInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("\"hello\""); + assertEquals(200, resp.getStatus()); + assertEquals(-32600, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + // --- Non-object element inside a batch → Invalid Request per element --- + + @Test + public void batchWithNestedArray_returnsInvalidRequestArray() throws Exception { + MockHttpServletResponse resp = doPost("[[]]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(1, body.size()); + assertEquals(-32600, body.get(0).get("error").get("code").asInt()); + assertTrue("id in batch error must be JSON null", body.get(0).get("id").isNull()); + } + + @Test + public void batchWithMixedObjectAndArray_objectProcessedArrayRejected() throws Exception { + byte[] singleResp = "{\"jsonrpc\":\"2.0\",\"result\":\"ok\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8); + doAnswer(inv -> { + OutputStream out = inv.getArgument(1); + out.write(singleResp); + return 0; + }).when(mockRpcServer).handleRequest(any(InputStream.class), any(OutputStream.class)); + + MockHttpServletResponse resp = doPost("[{\"id\":1}, []]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(2, body.size()); + assertEquals("ok", body.get(0).get("result").asText()); + assertEquals(-32600, body.get(1).get("error").get("code").asInt()); + } + + @Test + public void batchWithNumericAndStringElements_allGetInvalidRequest() throws Exception { + MockHttpServletResponse resp = doPost("[42, \"foo\", true]"); + assertEquals(200, resp.getStatus()); + JsonNode body = MAPPER.readTree(resp.getContentAsString()); + assertTrue("response must be a JSON array", body.isArray()); + assertEquals(3, body.size()); + for (int i = 0; i < 3; i++) { + assertEquals(-32600, body.get(i).get("error").get("code").asInt()); + } + } + + // --- StreamReadConstraints: maxNestingDepth and maxTokenCount must be enforced --- + + @Test + public void excessivelyNestedRequest_returnsParseError() throws Exception { + int limit = 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()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + + @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()); + assertEquals(-32700, + MAPPER.readTree(resp.getContentAsString()).get("error").get("code").asInt()); + } + // --- helpers --- private MockHttpServletResponse doPost(String body) throws Exception { diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java index c34d49d9009..8ea0f908899 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/GlobalRateLimiterTest.java @@ -1,5 +1,10 @@ package org.tron.core.services.ratelimiter; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.RateLimiter; @@ -9,6 +14,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.tron.common.TestConstants; import org.tron.core.config.args.Args; @@ -135,6 +143,104 @@ public void testPerIpLimitsAreIndependent() throws Exception { Assert.assertFalse(GlobalRateLimiter.tryAcquire(runtimeDataFor("2.2.2.2"))); } + /** + * acquire() must drain the IP limiter before the global limiter, mirroring + * tryAcquire(). A reversed order would let one chatty IP consume global + * quota even when its own per-IP budget is exhausted. + */ + @Test + public void testAcquireOrdersIpBeforeGlobal() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + RateLimiter ipMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + Cache seeded = CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build(); + seeded.put("10.0.0.1", ipMock); + injectCache(seeded); + + Assert.assertTrue(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + InOrder inOrder = Mockito.inOrder(ipMock, globalMock); + inOrder.verify(ipMock).acquire(); + inOrder.verify(globalMock).acquire(); + } + + /** + * If the IP limiter cannot be created (cache loader throws), acquire() + * returns false without consuming a global token — same fail-closed + * behaviour as tryAcquire(). + */ + @Test + public void testAcquireDoesNotConsumeGlobalWhenIpLoaderFails() throws Exception { + RateLimiter globalMock = Mockito.mock(RateLimiter.class); + injectRateLimiter(globalMock); + // RateLimiter.create(-1.0) throws IllegalArgumentException, so the + // cache loader fails and loadIpLimiter() returns null. + injectIpQps(-1.0); + injectCache(CacheBuilder.newBuilder() + .maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS).build()); + + Assert.assertFalse(GlobalRateLimiter.acquire(runtimeDataFor("10.0.0.1"))); + + Mockito.verify(globalMock, never()).acquire(); + } + + /** + * acquirePermit dispatches based on rate.limiter.apiNonBlocking: + * switch on → only tryAcquire runs; switch off → only acquire runs. + * These tests pin that contract on the static dispatcher; the matching + * default-method contract for IRateLimiter is covered in AdaptorTest. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(true); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.acquire(any()), never()); + } + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() throws Exception { + Args.getInstance().setRateLimiterApiNonBlocking(false); + RuntimeData rd = runtimeDataFor("10.0.0.1"); + + try (MockedStatic mock = mockStatic(GlobalRateLimiter.class)) { + mock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenCallRealMethod(); + mock.when(() -> GlobalRateLimiter.acquire(any())).thenReturn(true); + + Assert.assertTrue(GlobalRateLimiter.acquirePermit(rd)); + + mock.verify(() -> GlobalRateLimiter.acquire(any()), times(1)); + mock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + } + } + + private static void injectRateLimiter(RateLimiter rl) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("rateLimiter"); + f.setAccessible(true); + f.set(null, rl); + } + + private static void injectCache(Cache cache) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("cache"); + f.setAccessible(true); + f.set(null, cache); + } + + private static void injectIpQps(double qps) throws Exception { + Field f = GlobalRateLimiter.class.getDeclaredField("IP_QPS"); + f.setAccessible(true); + f.set(null, qps); + } + @AfterClass public static void destroy() { Args.clearParam(); diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java index 6cf02a25050..bbc365f3e0b 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/RateLimiterInterceptorTest.java @@ -95,13 +95,13 @@ public void setUp() throws Exception { @Test public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); verify(perEndpoint, never()).release(); } } @@ -112,13 +112,13 @@ public void testPerEndpointRejectedDoesNotConsumeGlobalQuota() { @Test public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { IRateLimiter perEndpoint = Mockito.mock(IRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(false); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), never()); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); } } @@ -129,11 +129,11 @@ public void testNonPreemptiblePerEndpointRejectedDoesNotConsumeGlobal() { @Test public void testGlobalRejectedReleasesPreemptiblePermit() { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(false); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); interceptor.interceptCall(call, headers, next); @@ -153,12 +153,12 @@ public void testGlobalRejectedReleasesPreemptiblePermit() { @Test public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); when(next.startCall(any(), any())).thenThrow(new RuntimeException("handler crash")); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); @@ -176,14 +176,14 @@ public void testStartCallExceptionReleasesPermitAndClosesCall() throws Exception @Test public void testListenerReleasesPermitOnComplete() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onComplete(); @@ -199,14 +199,14 @@ public void testListenerReleasesPermitOnComplete() throws Exception { @Test public void testListenerReleasesPermitOnCancel() throws Exception { IPreemptibleRateLimiter perEndpoint = Mockito.mock(IPreemptibleRateLimiter.class); - when(perEndpoint.tryAcquire(any(RuntimeData.class))).thenReturn(true); + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); container.add(KEY_RPC, METHOD_NAME, perEndpoint); ServerCall.Listener delegate = Mockito.mock(ServerCall.Listener.class); when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); ServerCall.Listener listener = interceptor.interceptCall(call, headers, next); listener.onCancel(); @@ -225,11 +225,11 @@ public void testNullRateLimiterConsultsOnlyGlobal() throws Exception { when(next.startCall(any(), any())).thenReturn(delegate); try (MockedStatic globalMock = mockStatic(GlobalRateLimiter.class)) { - globalMock.when(() -> GlobalRateLimiter.tryAcquire(any())).thenReturn(true); + globalMock.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(true); interceptor.interceptCall(call, headers, next); - globalMock.verify(() -> GlobalRateLimiter.tryAcquire(any()), times(1)); + globalMock.verify(() -> GlobalRateLimiter.acquirePermit(any()), times(1)); } } } diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java index 69a6c688200..5ab85a42bbf 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorTest.java @@ -4,12 +4,18 @@ import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.tron.common.TestConstants; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ReflectUtils; +import org.tron.core.config.args.Args; +import org.tron.core.services.ratelimiter.RuntimeData; import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; import org.tron.core.services.ratelimiter.strategy.GlobalPreemptibleStrategy; import org.tron.core.services.ratelimiter.strategy.IPQpsStrategy; @@ -17,6 +23,61 @@ public class AdaptorTest { + @Before + public void setUp() { + Args.setParam(new String[0], TestConstants.TEST_CONF); + } + + @AfterClass + public static void tearDown() { + Args.clearParam(); + } + + /** + * IRateLimiter.acquirePermit is a default method that dispatches based on + * rate.limiter.apiNonBlocking. The two cases below pin that contract: with + * the switch on, only tryAcquire is invoked; with the switch off, only + * acquire is invoked. Breaking either direction is a behavioural regression. + */ + @Test + public void testAcquirePermitDispatchesToTryAcquireWhenNonBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(true); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(1, limiter.tryAcquireCount); + Assert.assertEquals(0, limiter.acquireCount); + } + + @Test + public void testAcquirePermitDispatchesToAcquireWhenBlocking() { + Args.getInstance().setRateLimiterApiNonBlocking(false); + CountingRateLimiter limiter = new CountingRateLimiter(); + + Assert.assertTrue(limiter.acquirePermit(null)); + + Assert.assertEquals(0, limiter.tryAcquireCount); + Assert.assertEquals(1, limiter.acquireCount); + } + + private static final class CountingRateLimiter implements IRateLimiter { + int tryAcquireCount; + int acquireCount; + + @Override + public boolean tryAcquire(RuntimeData data) { + tryAcquireCount++; + return true; + } + + @Override + public boolean acquire(RuntimeData data) { + acquireCount++; + return true; + } + } + @Test public void testStrategy() { String paramString1 = "qps=5 notExist=6"; diff --git a/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..eb9fed8ff2c 100644 --- a/framework/src/test/java/org/tron/json/JsonTest.java +++ b/framework/src/test/java/org/tron/json/JsonTest.java @@ -16,6 +16,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. @@ -369,8 +370,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/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..8b79c8cb0d3 100644 --- a/protocol/src/main/protos/api/api.proto +++ b/protocol/src/main/protos/api/api.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package protocol; import "core/Tron.proto"; -import "google/api/annotations.proto"; import "core/contract/asset_issue_contract.proto"; import "core/contract/account_contract.proto";