diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef800e15ff..249bcaf28f1 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -279,6 +279,68 @@ jobs: echo "base_xmls=$BASE_XMLS" >> "$GITHUB_OUTPUT" echo "pr_xmls=$PR_XMLS" >> "$GITHUB_OUTPUT" + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Changed-line coverage (diff-cover) + id: diff-cover + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail + pip install --quiet 'diff-cover==9.2.0' + + # Ensure the base branch ref is available locally for diff-cover. + git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" + + PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort) + SRC_ROOTS=$(find . -type d -path '*/src/main/java' \ + -not -path './coverage/*' -not -path './.git/*' | sort) + if [ -z "$SRC_ROOTS" ]; then + echo "No src/main/java directories found; cannot run diff-cover." >&2 + exit 1 + fi + + set +e + diff-cover $PR_XMLS \ + --compare-branch="origin/${BASE_REF}" \ + --src-roots $SRC_ROOTS \ + --fail-under=0 \ + --json-report=diff-cover.json \ + --markdown-report=diff-cover.md + DIFF_RC=$? + set -e + + if [ ! -f diff-cover.json ]; then + echo "diff-cover did not produce JSON report (exit=${DIFF_RC})." >&2 + exit 1 + fi + + TOTAL_NUM_LINES=$(jq -r '.total_num_lines // 0' diff-cover.json) + if [ "${TOTAL_NUM_LINES}" = "0" ]; then + echo "No changed Java source lines; skipping changed-line gate." + echo "changed_line_coverage=NA" >> "$GITHUB_OUTPUT" + else + CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) + if [ -z "$CHANGED_LINE_COVERAGE" ]; then + echo "Unable to parse changed-line coverage from diff-cover.json." + exit 1 + fi + echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" + fi + + { + echo "### Changed-line Coverage (diff-cover)" + echo "" + if [ -f diff-cover.md ] && [ -s diff-cover.md ]; then + cat diff-cover.md + else + echo "_diff-cover produced no report._" + fi + } >> "$GITHUB_STEP_SUMMARY" + - name: Aggregate base coverage id: jacoco-base uses: madrapps/jacoco-report@v1.7.2 @@ -288,6 +350,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## Base Coverage Snapshot' update-comment: false @@ -300,6 +363,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## PR Code Coverage Report' update-comment: false @@ -307,7 +371,7 @@ jobs: env: BASE_OVERALL_RAW: ${{ steps.jacoco-base.outputs.coverage-overall }} PR_OVERALL_RAW: ${{ steps.jacoco-pr.outputs.coverage-overall }} - PR_CHANGED_RAW: ${{ steps.jacoco-pr.outputs.coverage-changed-files }} + CHANGED_LINE_RAW: ${{ steps.diff-cover.outputs.changed_line_coverage }} run: | set -euo pipefail @@ -329,7 +393,7 @@ jobs: # 1) Parse metrics from jacoco-report outputs BASE_OVERALL="$(sanitize "$BASE_OVERALL_RAW")" PR_OVERALL="$(sanitize "$PR_OVERALL_RAW")" - PR_CHANGED="$(sanitize "$PR_CHANGED_RAW")" + CHANGED_LINE="$(sanitize "$CHANGED_LINE_RAW")" if ! is_number "$BASE_OVERALL" || ! is_number "$PR_OVERALL"; then echo "Failed to parse coverage values: base='${BASE_OVERALL}', pr='${PR_OVERALL}'." @@ -340,18 +404,18 @@ jobs: DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }') DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}") - CHANGED_STATUS="SKIPPED (no changed coverage value)" - CHANGED_OK=1 - if [ -n "$PR_CHANGED" ] && [ "$PR_CHANGED" != "NaN" ]; then - if ! is_number "$PR_CHANGED"; then - echo "Failed to parse changed-files coverage: changed='${PR_CHANGED}'." - exit 1 - fi - CHANGED_OK=$(compare_float "${PR_CHANGED} > ${MIN_CHANGED}") - if [ "$CHANGED_OK" -eq 1 ]; then - CHANGED_STATUS="PASS (> ${MIN_CHANGED}%)" + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_OK=1 + CHANGED_LINE_STATUS="SKIPPED (no changed Java source lines)" + elif [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then + echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'." + exit 1 + else + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" else - CHANGED_STATUS="FAIL (<= ${MIN_CHANGED}%)" + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" fi fi @@ -361,13 +425,20 @@ jobs: OVERALL_STATUS="FAIL (< ${MAX_DROP}%)" fi + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_DISPLAY="NA" + else + CHANGED_LINE_DISPLAY="${CHANGED_LINE}%" + fi + METRICS_TEXT=$(cat <> "$GITHUB_STEP_SUMMARY" # 4) Decide CI pass/fail @@ -391,14 +464,9 @@ jobs: exit 1 fi - if [ -z "$PR_CHANGED" ] || [ "$PR_CHANGED" = "NaN" ]; then - echo "No changed-files coverage value detected, skip changed-files gate." - exit 0 - fi - - if [ "$CHANGED_OK" -ne 1 ]; then - echo "Coverage gate failed: changed files coverage must be > 60%." - echo "changed=${PR_CHANGED}%" + if [ "$CHANGED_LINE_OK" -ne 1 ]; then + echo "Coverage gate failed: changed-line coverage must be > 60%." + echo "changed-line=${CHANGED_LINE}%" exit 1 fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a9dd75824..ef67a81e3ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,7 +147,7 @@ We would like all developers to follow a standard development flow and coding st 2. Review the code before submission. 3. Run standardized tests. -`Sonar`-scanner and `Travis CI` continuous integration scanner will be automatically triggered when a pull request has been submitted. When a PR passes all the checks, the **java-tron** maintainers will then review the PR and offer feedback and modifications when necessary. Once adopted, the PR will be closed and merged into the `develop` branch. +`Sonar`-scanner and CI checks (GitHub Actions) will be automatically triggered when a pull request has been submitted. When a PR passes all the checks, the **java-tron** maintainers will then review the PR and offer feedback and modifications when necessary. Once adopted, the PR will be closed and merged into the `develop` branch. We are glad to receive your pull requests and will try our best to review them as soon as we can. Any pull request is welcome, even if it is for a typo. @@ -161,7 +161,7 @@ Please make sure your submission meets the following code style: - The code must have passed the Sonar scanner test. - The code has to be pulled from the `develop` branch. - The commit message should start with a verb, whose initial should not be capitalized. -- The commit message should be less than 50 characters in length. +- The commit message title should be between 10 and 72 characters in length. @@ -196,7 +196,7 @@ The message header is a single line that contains succinct description of the ch The `scope` can be anything specifying place of the commit change. For example: `framework`, `api`, `tvm`, `db`, `net`. For a full list of scopes, see [Type and Scope Reference](#type-and-scope-reference). You can use `*` if there isn't a more fitting scope. The subject contains a succinct description of the change: -1. Limit the subject line, which briefly describes the purpose of the commit, to 50 characters. +1. Limit the subject line, which briefly describes the purpose of the commit, to 72 characters (minimum 10). 2. Start with a verb and use first-person present-tense (e.g., use "change" instead of "changed" or "changes"). 3. Do not capitalize the first letter. 4. Do not end the subject line with a period. diff --git a/build.md b/build.md deleted file mode 100644 index 1f3671b2c7d..00000000000 --- a/build.md +++ /dev/null @@ -1,81 +0,0 @@ -# How to Build - -## Prepare dependencies - -* JDK 1.8 (JDK 1.9+ are not supported yet) -* On Linux Ubuntu system (e.g. Ubuntu 16.04.4 LTS), ensure that the machine has [__Oracle JDK 8__](https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-get-on-ubuntu-16-04), instead of having __Open JDK 8__ in the system. If you are building the source code by using __Open JDK 8__, you will get [__Build Failed__](https://github.com/tronprotocol/java-tron/issues/337) result. -* Open **UDP** ports for connection to the network -* **Minimum** 2 CPU Cores - -## Build and Deploy automatically using scripts - -- Please take a look at the [Tron Deployment Scripts](https://github.com/tronprotocol/TronDeployment) repository. - -## Getting the code with git - -* Use Git from the console, see the [Setting up Git](https://help.github.com/articles/set-up-git/) and [Fork a Repo](https://help.github.com/articles/fork-a-repo/) articles. -* `develop` branch: the newest code -* `master` branch: more stable than develop. -In the shell command, type: - ```bash - git clone https://github.com/tronprotocol/java-tron.git - git checkout -t origin/master - ``` - -* For Mac, you can also install **[GitHub for Mac](https://mac.github.com/)** then **[fork and clone our repository](https://guides.github.com/activities/forking/)**. - -* If you'd rather not use Git, **[Download the ZIP](https://github.com/tronprotocol/java-tron/archive/develop.zip)** - -## Including java-tron as dependency - -If you don't want to checkout the code and build the project, you can include it directly as a dependency. - -**Using gradle:** - -``` -repositories { - maven { url 'https://jitpack.io' } -} -dependencies { - implementation 'com.github.tronprotocol:java-tron:develop-SNAPSHOT' -} -``` - -**Using maven:** - -```xml - - - jitpack.io - https://jitpack.io - - - - - com.github.tronprotocol - java-tron - develop-SNAPSHOT - - -``` - -## Building from source code - -- **Building using the console:** - - ```bash - cd java-tron - ./gradlew build - ``` - -- **Building using [IntelliJ IDEA](https://www.jetbrains.com/idea/) (community version is enough):** - - **Please run `./gradlew build` once to build the protocol files** - - 1. Start IntelliJ. - Select `File` -> `Open`, then locate to the java-tron folder which you have git cloned to your local drive. Then click `Open` button on the right bottom. - 2. Check on `Use auto-import` on the `Import Project from Gradle` dialog. Select JDK 1.8 in the `Gradle JVM` option. Then click `OK`. - 3. IntelliJ will import the project and start gradle syncing, which will take several minutes, depending on your network connection and your IntelliJ configuration - 4. Enable Annotations, `Preferences` -> Search `annotations` -> check `Enable Annotation Processing`. - 5. When the syncing finishes, select `Gradle` -> `Tasks` -> `build`, and then double click `build` option. - diff --git a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java index 40762568c82..237ab8dbaad 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java @@ -76,13 +76,25 @@ public void reset() { @Override public void close() { logger.info("******** Begin to close {}. ********", getName()); + doClose(); + logger.info("******** End to close {}. ********", getName()); + } + + /** + * Releases writeOptions and dbSource (best-effort, exceptions logged at WARN). + * Subclasses with extra resources should override {@link #close()} and call + * {@code doClose()} directly — not {@code super.close()} — to avoid duplicated logs. + */ + protected void doClose() { try { writeOptions.close(); + } catch (Exception e) { + logger.warn("Failed to close writeOptions in {}.", getName(), e); + } + try { dbSource.closeDB(); } catch (Exception e) { - logger.warn("Failed to close {}.", getName(), e); - } finally { - logger.info("******** End to close {}. ********", getName()); + logger.warn("Failed to close dbSource in {}.", getName(), e); } } diff --git a/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java b/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java index f027bd02664..fa8092273d2 100644 --- a/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java +++ b/chainbase/src/main/java/org/tron/core/store/CheckPointV2Store.java @@ -62,20 +62,16 @@ public void updateByBatch(Map rows) { this.dbSource.updateByBatch(rows, writeOptions); } - /** - * close the database. - */ @Override public void close() { logger.debug("******** Begin to close {}. ********", getName()); try { writeOptions.close(); - dbSource.closeDB(); } catch (Exception e) { - logger.warn("Failed to close {}.", getName(), e); - } finally { - logger.debug("******** End to close {}. ********", getName()); + logger.warn("Failed to close writeOptions in {}.", getName(), e); } + doClose(); + logger.debug("******** End to close {}. ********", getName()); } } 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 a73158a718a..6dc5f0ae52b 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -211,11 +211,19 @@ public class CommonParameter { @Getter @Setter public long maxConnectionAgeInMillis; + // Refers to RPC (gRPC) max message size; see httpMaxMessageSize / jsonRpcMaxMessageSize + // below for the HTTP / JSON-RPC counterparts. @Getter @Setter public int maxMessageSize; @Getter @Setter + public long httpMaxMessageSize; + @Getter + @Setter + public long jsonRpcMaxMessageSize; + @Getter + @Setter public int maxHeaderListSize; @Getter @Setter diff --git a/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java b/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java index 9ae7eb7db68..41388adeb7b 100644 --- a/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java +++ b/common/src/main/java/org/tron/common/parameter/RateLimiterInitialization.java @@ -74,6 +74,12 @@ public HttpRateLimiterItem(ConfigObject asset) { strategy = asset.get("strategy").unwrapped().toString(); params = asset.get("paramString").unwrapped().toString(); } + + public HttpRateLimiterItem(String component, String strategy, String params) { + this.component = component; + this.strategy = strategy; + this.params = params; + } } @@ -93,5 +99,11 @@ public RpcRateLimiterItem(ConfigObject asset) { strategy = asset.get("strategy").unwrapped().toString(); params = asset.get("paramString").unwrapped().toString(); } + + public RpcRateLimiterItem(String component, String strategy, String params) { + this.component = component; + this.strategy = strategy; + this.params = params; + } } } \ No newline at end of file diff --git a/common/src/main/java/org/tron/core/config/Configuration.java b/common/src/main/java/org/tron/core/config/Configuration.java index d75fc8430f8..80735290b8c 100644 --- a/common/src/main/java/org/tron/core/config/Configuration.java +++ b/common/src/main/java/org/tron/core/config/Configuration.java @@ -48,7 +48,8 @@ public static com.typesafe.config.Config getByFileName( private static void resolveConfigFile(String fileName, File confFile) { if (confFile.exists()) { - config = ConfigFactory.parseFile(confFile); + config = ConfigFactory.parseFile(confFile) + .withFallback(ConfigFactory.defaultReference()); } else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName) != null) { config = ConfigFactory.load(fileName); diff --git a/common/src/main/java/org/tron/core/config/args/BlockConfig.java b/common/src/main/java/org/tron/core/config/args/BlockConfig.java new file mode 100644 index 00000000000..4746f390e0c --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/BlockConfig.java @@ -0,0 +1,55 @@ +package org.tron.core.config.args; + +import static org.tron.core.Constant.DEFAULT_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.Constant.MAX_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.Constant.MIN_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.exception.TronError.ErrCode.PARAMETER_INIT; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.exception.TronError; + +/** + * Block configuration bean. Field names match config.conf keys under the "block" section. + */ +@Slf4j +@Getter +@Setter +public class BlockConfig { + + private boolean needSyncCheck = false; + private long maintenanceTimeInterval = 21600000L; + private long proposalExpireTime = DEFAULT_PROPOSAL_EXPIRE_TIME; + private int checkFrozenTime = 1; + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create BlockConfig from the "block" section of the application config. + * Also checks that committee.proposalExpireTime is not used (must use block.proposalExpireTime). + */ + public static BlockConfig fromConfig(Config config) { + // Reject legacy committee.proposalExpireTime location + if (config.hasPath("committee.proposalExpireTime")) { + throw new TronError("It is not allowed to configure committee.proposalExpireTime in " + + "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT); + } + + Config blockSection = config.getConfig("block"); + BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class); + blockConfig.postProcess(); + return blockConfig; + } + + private void postProcess() { + if (proposalExpireTime <= MIN_PROPOSAL_EXPIRE_TIME + || proposalExpireTime >= MAX_PROPOSAL_EXPIRE_TIME) { + throw new TronError("The value[block.proposalExpireTime] is only allowed to " + + "be greater than " + MIN_PROPOSAL_EXPIRE_TIME + " and less than " + + MAX_PROPOSAL_EXPIRE_TIME + "!", PARAMETER_INIT); + } + } +} 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 new file mode 100644 index 00000000000..0f94e7a59eb --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/CommitteeConfig.java @@ -0,0 +1,165 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Committee (governance) configuration bean. + * Field names match config.conf keys under the "committee" section. + * All fields are governance proposal toggles, default 0 (disabled). + */ +@Slf4j +@Getter +@Setter +@SuppressWarnings("unused") // setters used by ConfigBeanFactory via reflection +public class CommitteeConfig { + + private long allowCreationOfContracts = 0; + private long allowMultiSign = 0; + private long allowAdaptiveEnergy = 0; + private long allowDelegateResource = 0; + private long allowSameTokenName = 0; + private long allowTvmTransferTrc10 = 0; + private long allowTvmConstantinople = 0; + private long allowTvmSolidity059 = 0; + private long forbidTransferToContract = 0; + private long allowShieldedTRC20Transaction = 0; + private long allowMarketTransaction = 0; + private long allowTransactionFeePool = 0; + private long allowBlackHoleOptimization = 0; + private long allowNewResourceModel = 0; + private long allowTvmIstanbul = 0; + 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; } + private long allowTvmFreeze = 0; + private long allowTvmVote = 0; + private long allowTvmLondon = 0; + private long allowTvmCompatibleEvm = 0; + private long allowHigherLimitForMaxCpuTimeOfOneTx = 0; + private long allowNewRewardAlgorithm = 0; + private long allowOptimizedReturnValueOfChainId = 0; + private long allowTvmShangHai = 0; + private long allowOldRewardOpt = 0; + private long allowEnergyAdjustment = 0; + private long allowStrictMath = 0; + private long consensusLogicOptimization = 0; + private long allowTvmCancun = 0; + private long allowTvmBlob = 0; + private long allowTvmOsaka = 0; + private long unfreezeDelayDays = 0; + private long allowReceiptsMerkleRoot = 0; + private long allowAccountAssetOptimization = 0; + private long allowAssetOptimization = 0; + private long allowNewReward = 0; + private long memoFee = 0; + private long allowDelegateOptimization = 0; + private long allowDynamicEnergy = 0; + private long dynamicEnergyThreshold = 0; + private long dynamicEnergyIncreaseFactor = 0; + 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"); + + 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; + } + + private void postProcess() { + // clamp unfreezeDelayDays to 0-365 + if (unfreezeDelayDays < 0) { + unfreezeDelayDays = 0; + } + if (unfreezeDelayDays > 365) { + unfreezeDelayDays = 365; + } + + // clamp allowDelegateOptimization to 0-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; } + + // clamp dynamicEnergyThreshold to 0-100_000_000_000_000_000 + 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; } + + // clamp dynamicEnergyMaxFactor to 0-100_000 + 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; } + + // clamp memoFee to 0-1_000_000_000 + 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 + && allowNewReward != 1 && allowTvmVote != 1) { + throw new IllegalArgumentException( + "At least one of the following proposals is required to be opened first: " + + "committee.allowNewRewardAlgorithm = 1" + + " or committee.allowNewReward = 1" + + " or committee.allowTvmVote = 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 new file mode 100644 index 00000000000..ac1731de2dc --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/EventConfig.java @@ -0,0 +1,134 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Event subscribe configuration bean. + * Field names match config.conf keys under "event.subscribe". + */ +@Slf4j +@Getter +@Setter +public class EventConfig { + + private boolean enable = false; + private int version = 0; + private long startSyncBlockNum = 0; + private String path = ""; + private String server = ""; + private String dbconfig = ""; + private boolean contractParse = true; + @Getter(lombok.AccessLevel.NONE) + @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) + @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 int bindport = 5555; + private int sendqueuelength = 1000; + } + + @Getter + @Setter + public static class TopicConfig { + private String triggerName = ""; + private boolean enable = false; + private String topic = ""; + private boolean solidified = false; + private boolean ethCompatible = false; + private boolean redundancy = false; + } + + @Getter + @Setter + public static class FilterConfig { + private String fromblock = ""; + private String toblock = ""; + private List contractAddress = new ArrayList<>(); + private List contractTopic = new ArrayList<>(); + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * 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. + */ + 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"); + 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"); + } + + // manually bind topics — each item may have optional fields + 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); + } + } + + return ec; + } +} diff --git a/common/src/main/java/org/tron/core/config/args/GenesisConfig.java b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java new file mode 100644 index 00000000000..a17e06d5c0f --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/GenesisConfig.java @@ -0,0 +1,50 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Genesis block configuration bean. + * Field names match config.conf keys under "genesis.block". + * Assets and witnesses are stored as raw bean lists; address decoding + * (e.g. Base58Check) is done in the bridge method, not here. + */ +@Slf4j +@Getter +@Setter +public class GenesisConfig { + + private String timestamp = ""; + private String parentHash = ""; + private List assets = new ArrayList<>(); + private List witnesses = new ArrayList<>(); + + @Getter + @Setter + public static class AssetConfig { + private String accountName = ""; + private String accountType = ""; + private String address = ""; + private String balance = ""; + } + + @Getter + @Setter + public static class WitnessConfig { + private String address = ""; + private String url = ""; + private long voteCount = 0; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + public static GenesisConfig fromConfig(Config config) { + Config section = config.getConfig("genesis.block"); + return ConfigBeanFactory.create(section, GenesisConfig.class); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java b/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java new file mode 100644 index 00000000000..8a2cd2ce9e4 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/LocalWitnessConfig.java @@ -0,0 +1,35 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Local witness configuration bean. + * Reads top-level config keys: localwitness, localWitnessAccountAddress, localwitnesskeystore. + * These are not under a sub-section — they are at the root of config.conf. + */ +@Slf4j +@Getter +public class LocalWitnessConfig { + + private List privateKeys = new ArrayList<>(); + private String accountAddress = null; + private List keystores = new ArrayList<>(); + + public static LocalWitnessConfig fromConfig(Config config) { + LocalWitnessConfig lw = new LocalWitnessConfig(); + if (config.hasPath("localwitness")) { + lw.privateKeys = config.getStringList("localwitness"); + } + if (config.hasPath("localWitnessAccountAddress")) { + lw.accountAddress = config.getString("localWitnessAccountAddress"); + } + if (config.hasPath("localwitnesskeystore")) { + lw.keystores = config.getStringList("localwitnesskeystore"); + } + return lw; + } +} diff --git a/common/src/main/java/org/tron/core/config/args/MetricsConfig.java b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java new file mode 100644 index 00000000000..5b504acdd1c --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/MetricsConfig.java @@ -0,0 +1,47 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Metrics configuration bean. Field names match config.conf keys under "node.metrics". + * Contains nested sub-beans for prometheus and influxdb sections. + */ +@Slf4j +@Getter +@Setter +public class MetricsConfig { + + private boolean storageEnable = false; + private PrometheusConfig prometheus = new PrometheusConfig(); + private InfluxDbConfig influxdb = new InfluxDbConfig(); + + @Getter + @Setter + public static class PrometheusConfig { + private boolean enable = false; + private int port = 9527; + } + + @Getter + @Setter + public static class InfluxDbConfig { + private String ip = ""; + private int port = 8086; + private String database = "metrics"; + private int metricsReportInterval = 10; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + /** + * Create MetricsConfig from the "node.metrics" section of the application config. + */ + public static MetricsConfig fromConfig(Config config) { + Config section = config.getConfig("node.metrics"); + return ConfigBeanFactory.create(section, MetricsConfig.class); + } +} diff --git a/common/src/main/java/org/tron/core/config/args/MiscConfig.java b/common/src/main/java/org/tron/core/config/args/MiscConfig.java new file mode 100644 index 00000000000..f6c3b200b80 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/MiscConfig.java @@ -0,0 +1,70 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.Constant; + +/** + * Miscellaneous small config domains that don't warrant their own bean class. + * Covers: storage (partial), trx, energy, crypto, seed, actuator. + * + *

These use manual reads because they span multiple unrelated config.conf + * top-level sections and some have non-standard key naming (e.g. "enery" typo). + */ +@Slf4j +@Getter +public class MiscConfig { + + private boolean needToUpdateAsset = true; + private boolean historyBalanceLookup = false; + private String trxReferenceBlock = "solid"; + private long trxExpirationTimeInMilliseconds = Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME; + private long blockNumForEnergyLimit = 4727890L; + private String cryptoEngine = Constant.ECKey_ENGINE; + private List seedNodeIpList = new ArrayList<>(); + private Set actuatorWhitelist = Collections.emptySet(); + + public static MiscConfig fromConfig(Config config) { + MiscConfig mc = new MiscConfig(); + + // storage + mc.needToUpdateAsset = !config.hasPath("storage.needToUpdateAsset") + || config.getBoolean("storage.needToUpdateAsset"); + mc.historyBalanceLookup = config.hasPath("storage.balance.history.lookup") + && config.getBoolean("storage.balance.history.lookup"); + + // trx + mc.trxReferenceBlock = config.hasPath("trx.reference.block") + ? config.getString("trx.reference.block") : "solid"; + String trxExpirationKey = "trx.expiration.timeInMilliseconds"; + if (config.hasPath(trxExpirationKey) + && config.getLong(trxExpirationKey) > 0) { + mc.trxExpirationTimeInMilliseconds = config.getLong(trxExpirationKey); + } + + // energy (note: config key has typo "enery" — preserved for backward compat) + mc.blockNumForEnergyLimit = config.hasPath("enery.limit.block.num") + ? config.getInt("enery.limit.block.num") : 4727890L; + + // crypto + mc.cryptoEngine = config.hasPath("crypto.engine") + ? config.getString("crypto.engine") : Constant.ECKey_ENGINE; + + // seed node + mc.seedNodeIpList = config.hasPath("seed.node.ip.list") + ? config.getStringList("seed.node.ip.list") : new ArrayList<>(); + + // actuator + mc.actuatorWhitelist = config.hasPath("actuator.whitelist") + ? new HashSet<>(config.getStringList("actuator.whitelist")) : Collections.emptySet(); + + return mc; + } +} 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 new file mode 100644 index 00000000000..12020cb1211 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -0,0 +1,536 @@ +package org.tron.core.config.args; + +import static org.tron.core.config.Parameter.ChainConstant.MAX_ACTIVE_WITNESS_NUM; +import static org.tron.core.exception.TronError.ErrCode.PARAMETER_INIT; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.exception.TronError; + +// Node configuration bean for the "node" section of config.conf. +// 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. +@Slf4j +@Getter +@Setter +@SuppressWarnings("unused") // setters used by ConfigBeanFactory via reflection +public class NodeConfig { + + // ---- Flat scalar fields (auto-bound by ConfigBeanFactory) ---- + private String trustNode = ""; + private boolean walletExtensionApi = false; + private int syncFetchBatchNum = 2000; + private int validateSignThreadNum = 0; // 0 = auto (availableProcessors) + private int maxConnections = 30; + private int minConnections = 8; + private int minActiveConnections = 3; + private int maxConnectionsWithSameIp = 2; + private int maxHttpConnectNumber = 50; + private int minParticipationRate = 0; + private boolean openPrintLog = true; + private boolean openTransactionSort = false; + private int maxTps = 1000; + // 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; } + + // node.discovery.* — HOCON merges into node { discovery { ... } }, auto-bound + private DiscoveryConfig discovery = new DiscoveryConfig(); + + // node.shutdown.* uses PascalCase keys (BlockTime, BlockHeight, BlockCount) + // that don't match JavaBean naming. Excluded, read manually. + @Getter(lombok.AccessLevel.NONE) + @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 discovery.getExternal().getIp(); } + public String getShutdownBlockTime() { return shutdownBlockTime; } + public long getShutdownBlockHeight() { return shutdownBlockHeight; } + public long getShutdownBlockCount() { return shutdownBlockCount; } + private int inactiveThreshold = 600; + private boolean metricsEnable = false; + private int blockProducedTimeOut = 50; + private int netMaxTrxPerSecond = 700; + private boolean nodeDetectEnable = false; + private boolean enableIpv6 = false; + private boolean effectiveCheckEnable = false; + private int maxFastForwardNum = 4; + private int tcpNettyWorkThreadNum = 0; + private int udpNettyWorkThreadNum = 1; + private ValidContractProtoConfig validContractProto = new ValidContractProtoConfig(); + private int shieldedTransInPendingMaxCounts = 10; + private long blockCacheTimeout = 60; + private long receiveTcpMinDataLength = 2048; + private ChannelConfig channel = new ChannelConfig(); + private int maxTransactionPendingSize = 2000; + private long pendingTransactionTimeout = 60000; + private int agreeNodeCount = 0; + private boolean openHistoryQueryWhenLiteFN = false; + private boolean unsolidifiedBlockCheck = false; + private int maxUnsolidifiedBlocks = 54; + private String zenTokenId = "000000"; + private boolean allowShieldedTransactionApi = true; + 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(); + private ConnectionConfig connection = new ConnectionConfig(); + private FetchBlockConfig fetchBlock = new FetchBlockConfig(); + private SolidityConfig solidity = new SolidityConfig(); + + // Convenience getters for backward compatibility with applyNodeConfig + public int getListenPort() { return listen.getPort(); } + public int getConnectionTimeout() { return connection.getTimeout(); } + public int getFetchBlockTimeout() { return fetchBlock.getTimeout(); } + public int getSolidityThreads() { return solidity.getThreads(); } + public int getChannelReadTimeout() { return channel.getRead().getTimeout(); } + public int getValidContractProtoThreads() { return validContractProto.getThreads(); } + + // ---- List fields (manually read) ---- + private List active = new ArrayList<>(); + private List passive = new ArrayList<>(); + private List fastForward = new ArrayList<>(); + private List disabledApi = new ArrayList<>(); + + // ---- Sub-object fields ---- + private P2pConfig p2p = new P2pConfig(); + private HttpConfig http = new HttpConfig(); + private RpcConfig rpc = new RpcConfig(); + private JsonRpcConfig jsonrpc = new JsonRpcConfig(); + private NodeBackupConfig backup = new NodeBackupConfig(); + private DynamicConfigSection dynamicConfig = new DynamicConfigSection(); + private DnsConfig dns = new DnsConfig(); + + // =========================================================================== + // Inner static classes for sub-beans + // =========================================================================== + + // ---- Sub-beans for dot-notation config keys ---- + // HOCON merges dot-notation into nested objects, ConfigBeanFactory auto-binds + + @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 ConnectionConfig { + private int timeout = 2; + } + + @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 ChannelConfig { + private ReadConfig read = new ReadConfig(); + + @Getter + @Setter + public static class ReadConfig { + private int timeout = 0; + } + } + + @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; + // 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 flowControlWindow = 1048576; + private long maxConnectionIdleInMillis = Long.MAX_VALUE; + private long maxConnectionAgeInMillis = Long.MAX_VALUE; + private int maxMessageSize = 4194304; + private int maxHeaderListSize = 8192; + private int maxRstStream = 0; + private int secondsPerWindow = 0; + private int minEffectiveConnection = 1; + private boolean reflectionService = false; + private boolean trxCacheEnable = false; + } + + @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; + private long maxMessageSize = 4194304; + } + + @Getter + @Setter + public static class NodeBackupConfig { + private int priority = 0; + private int port = 10001; + private int keepAliveInterval = 3000; + private List members = new ArrayList<>(); + } + + @Getter + @Setter + public static class DynamicConfigSection { + private boolean enable = false; + private long checkInterval = 600; + } + + @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 String serverType = ""; + private String accessKeyId = ""; + private String accessKeySecret = ""; + private String aliyunDnsEndpoint = ""; + private String awsRegion = ""; + 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, connection.timeout, 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"); + + // 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. + 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")) { + nc.maxConnections = section.getInt("maxActiveNodes"); + if (section.hasPath("connectFactor")) { + nc.minConnections = (int) (nc.maxConnections * section.getDouble("connectFactor")); + } + if (section.hasPath("activeConnectFactor")) { + nc.minActiveConnections = (int) (nc.maxConnections + * section.getDouble("activeConnectFactor")); + } + } + if (section.hasPath("maxActiveNodesWithSameIp")) { + nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); + } + + // Legacy key fallback: node.fullNodeAllowShieldedTransaction -> allowShieldedTransactionApi. + // reference.conf does not ship the legacy key, so hasPath here reliably means the user + // set it in their config. When present, it overrides the modern key. + if (section.hasPath("fullNodeAllowShieldedTransaction")) { + nc.allowShieldedTransactionApi = section.getBoolean("fullNodeAllowShieldedTransaction"); + logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " + + "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; + + + nc.postProcess(); + return nc; + } + + /** + * Post-processing: clamping, dynamic defaults, and cross-field validation. + * Runs after ConfigBeanFactory binding and manual field reads. + */ + private void postProcess() { + // rpcThreadNum: 0 = auto-detect + if (rpc.thread == 0) { + rpc.thread = (Runtime.getRuntime().availableProcessors() + 1) / 2; + } + + // validateSignThreadNum: 0 = auto-detect + if (validateSignThreadNum == 0) { + validateSignThreadNum = Runtime.getRuntime().availableProcessors(); + } + + // solidityThreads: 0 = auto-detect + if (solidity.threads == 0) { + solidity.threads = Runtime.getRuntime().availableProcessors(); + } + + // validContractProto.threads: 0 = auto-detect (matches develop Args.java:743-746) + if (validContractProto.threads == 0) { + validContractProto.threads = Runtime.getRuntime().availableProcessors(); + } + + // syncFetchBatchNum: clamp to [100, 2000] + if (syncFetchBatchNum > 2000) { + syncFetchBatchNum = 2000; + } + if (syncFetchBatchNum < 100) { + syncFetchBatchNum = 100; + } + + // blockProducedTimeOut: clamp to [30, 100] + if (blockProducedTimeOut < 30) { + blockProducedTimeOut = 30; + } + if (blockProducedTimeOut > 100) { + blockProducedTimeOut = 100; + } + + // inactiveThreshold: minimum 1 + if (inactiveThreshold < 1) { + inactiveThreshold = 1; + } + + // maxFastForwardNum: clamp to [1, MAX_ACTIVE_WITNESS_NUM] + if (maxFastForwardNum > MAX_ACTIVE_WITNESS_NUM) { + maxFastForwardNum = MAX_ACTIVE_WITNESS_NUM; + } + if (maxFastForwardNum < 1) { + maxFastForwardNum = 1; + } + + // agreeNodeCount: 0 = auto (2/3 + 1 of witnesses), clamp to max + if (agreeNodeCount == 0) { + agreeNodeCount = MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; + } + if (agreeNodeCount > MAX_ACTIVE_WITNESS_NUM) { + agreeNodeCount = MAX_ACTIVE_WITNESS_NUM; + } + + // dynamicConfigCheckInterval: minimum 600 + if (dynamicConfig.checkInterval <= 0) { + dynamicConfig.checkInterval = 600; + } + } + + // =========================================================================== + // Helper methods for safe config reads + // =========================================================================== + + private static int getInt(Config config, String path, int defaultValue) { + return config.hasPath(path) ? config.getInt(path) : defaultValue; + } + + private static long getLong(Config config, String path, long defaultValue) { + return config.hasPath(path) ? config.getLong(path) : defaultValue; + } + + private static boolean getBool(Config config, String path, boolean defaultValue) { + return config.hasPath(path) ? config.getBoolean(path) : defaultValue; + } + + 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)); + } + } + 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); + } + return value; + } + +} 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 new file mode 100644 index 00000000000..eed5ef1898b --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/RateLimiterConfig.java @@ -0,0 +1,75 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * Rate limiter configuration bean. + * Field names match config.conf keys under "rate.limiter". + */ +@Slf4j +@Getter +@Setter +public class RateLimiterConfig { + + private GlobalConfig global = new GlobalConfig(); + private P2pRateLimitConfig p2p = new P2pRateLimitConfig(); + private List http = new ArrayList<>(); + private List rpc = new ArrayList<>(); + + @Getter + @Setter + public static class GlobalConfig { + private int qps = 50000; + private IpConfig ip = new IpConfig(); + private ApiConfig api = new ApiConfig(); + + @Getter + @Setter + public static class IpConfig { + private int qps = 10000; + } + + @Getter + @Setter + public static class ApiConfig { + private int qps = 1000; + } + } + + @Getter + @Setter + public static class P2pRateLimitConfig { + private double syncBlockChain = 3.0; + private double fetchInvData = 3.0; + private double disconnect = 1.0; + } + + @Getter + @Setter + public static class HttpRateLimitItem { + private String component = ""; + private String strategy = ""; + private String paramString = ""; + } + + @Getter + @Setter + public static class RpcRateLimitItem { + private String component = ""; + private String strategy = ""; + private String paramString = ""; + } + + // Defaults come from reference.conf (loaded globally via Configuration.java) + + public static RateLimiterConfig fromConfig(Config config) { + Config section = config.getConfig("rate.limiter"); + return ConfigBeanFactory.create(section, RateLimiterConfig.class); + } +} 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 116074c62ee..782a0ef07c8 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 @@ -18,7 +18,6 @@ import com.google.common.collect.Maps; import com.google.protobuf.ByteString; import com.typesafe.config.Config; -import com.typesafe.config.ConfigObject; import java.io.File; import java.util.List; import java.util.Map; @@ -26,7 +25,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.iq80.leveldb.CompressionType; import org.iq80.leveldb.Options; import org.tron.common.cache.CacheStrategies; @@ -46,59 +44,12 @@ @Slf4j(topic = "db") public class Storage { - /** - * Keys (names) of database config - */ - private static final String DB_DIRECTORY_CONFIG_KEY = "storage.db.directory"; - private static final String DB_ENGINE_CONFIG_KEY = "storage.db.engine"; - private static final String DB_SYNC_CONFIG_KEY = "storage.db.sync"; - private static final String INDEX_DIRECTORY_CONFIG_KEY = "storage.index.directory"; - private static final String INDEX_SWITCH_CONFIG_KEY = "storage.index.switch"; - private static final String TRANSACTIONHISTORY_SWITCH_CONFIG_KEY = "storage.transHistory.switch"; - private static final String ESTIMATED_TRANSACTIONS_CONFIG_KEY = - "storage.txCache.estimatedTransactions"; - private static final String SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY = "storage.snapshot.maxFlushCount"; - private static final String PROPERTIES_CONFIG_KEY = "storage.properties"; - private static final String PROPERTIES_CONFIG_DB_KEY = "storage"; - private static final String PROPERTIES_CONFIG_DEFAULT_KEY = "default"; - private static final String PROPERTIES_CONFIG_DEFAULT_M_KEY = "defaultM"; - private static final String PROPERTIES_CONFIG_DEFAULT_L_KEY = "defaultL"; - private static final String DEFAULT_TRANSACTIONHISTORY_SWITCH = "on"; - - private static final String NAME_CONFIG_KEY = "name"; - private static final String PATH_CONFIG_KEY = "path"; - private static final String CREATE_IF_MISSING_CONFIG_KEY = "createIfMissing"; - private static final String PARANOID_CHECKS_CONFIG_KEY = "paranoidChecks"; - private static final String VERITY_CHECK_SUMS_CONFIG_KEY = "verifyChecksums"; - private static final String COMPRESSION_TYPE_CONFIG_KEY = "compressionType"; - private static final String BLOCK_SIZE_CONFIG_KEY = "blockSize"; - private static final String WRITE_BUFFER_SIZE_CONFIG_KEY = "writeBufferSize"; - private static final String CACHE_SIZE_CONFIG_KEY = "cacheSize"; - private static final String MAX_OPEN_FILES_CONFIG_KEY = "maxOpenFiles"; - private static final String EVENT_SUBSCRIBE_CONTRACT_PARSE = "event.subscribe.contractParse"; - - private static final String CHECKPOINT_VERSION_KEY = "storage.checkpoint.version"; - private static final String CHECKPOINT_SYNC_KEY = "storage.checkpoint.sync"; - - private static final String CACHE_STRATEGIES = "storage.cache.strategies"; - public static final String TX_CACHE_INIT_OPTIMIZATION = "storage.txCache.initOptimization"; - - private static final String MERKLE_ROOT = "storage.merkleRoot"; - - /** - * Default values of directory - */ - private static final String DEFAULT_DB_ENGINE = "LEVELDB"; - private static final boolean DEFAULT_DB_SYNC = false; - private static final boolean DEFAULT_EVENT_SUBSCRIBE_CONTRACT_PARSE = true; - private static final String DEFAULT_DB_DIRECTORY = "database"; - private static final String DEFAULT_INDEX_DIRECTORY = "index"; private static final String DEFAULT_INDEX_SWITCH = "on"; - private static final int DEFAULT_CHECKPOINT_VERSION = 1; - private static final boolean DEFAULT_CHECKPOINT_SYNC = true; - private static final int DEFAULT_ESTIMATED_TRANSACTIONS = 1000; - private static final int DEFAULT_SNAPSHOT_MAX_FLUSH_COUNT = 1; - private Config storage; + + // Optional per-tier LevelDB option overrides, read from StorageConfig bean + private StorageConfig.DbOptionOverride defaultDbOption; + private StorageConfig.DbOptionOverride defaultMDbOption; + private StorageConfig.DbOptionOverride defaultLDbOption; /** * Database storage directory: /path/to/{dbDirectory} @@ -172,92 +123,13 @@ public class Storage { // db root private final Map dbRoots = Maps.newConcurrentMap(); - public static String getDbEngineFromConfig(final Config config) { - return config.hasPath(DB_ENGINE_CONFIG_KEY) - ? config.getString(DB_ENGINE_CONFIG_KEY) : DEFAULT_DB_ENGINE; - } - - public static Boolean getDbVersionSyncFromConfig(final Config config) { - return config.hasPath(DB_SYNC_CONFIG_KEY) - ? config.getBoolean(DB_SYNC_CONFIG_KEY) : DEFAULT_DB_SYNC; - } - - public static int getSnapshotMaxFlushCountFromConfig(final Config config) { - if (!config.hasPath(SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY)) { - return DEFAULT_SNAPSHOT_MAX_FLUSH_COUNT; - } - int maxFlushCountConfig = config.getInt(SNAPSHOT_MAX_FLUSH_COUNT_CONFIG_KEY); - if (maxFlushCountConfig <= 0) { - throw new IllegalArgumentException("MaxFlushCount value can not be negative or zero!"); - } - if (maxFlushCountConfig > 500) { - throw new IllegalArgumentException("MaxFlushCount value must not exceed 500!"); - } - return maxFlushCountConfig; - } - - public static Boolean getContractParseSwitchFromConfig(final Config config) { - return config.hasPath(EVENT_SUBSCRIBE_CONTRACT_PARSE) - ? config.getBoolean(EVENT_SUBSCRIBE_CONTRACT_PARSE) - : DEFAULT_EVENT_SUBSCRIBE_CONTRACT_PARSE; - } - - public static String getDbDirectoryFromConfig(final Config config) { - return config.hasPath(DB_DIRECTORY_CONFIG_KEY) - ? config.getString(DB_DIRECTORY_CONFIG_KEY) : DEFAULT_DB_DIRECTORY; - } - - public static String getIndexDirectoryFromConfig(final Config config) { - return config.hasPath(INDEX_DIRECTORY_CONFIG_KEY) - ? config.getString(INDEX_DIRECTORY_CONFIG_KEY) : DEFAULT_INDEX_DIRECTORY; - } - - public static String getIndexSwitchFromConfig(final Config config) { - return config.hasPath(INDEX_SWITCH_CONFIG_KEY) - && StringUtils.isNotEmpty(config.getString(INDEX_SWITCH_CONFIG_KEY)) - ? config.getString(INDEX_SWITCH_CONFIG_KEY) : DEFAULT_INDEX_SWITCH; - } - - public static String getTransactionHistorySwitchFromConfig(final Config config) { - return config.hasPath(TRANSACTIONHISTORY_SWITCH_CONFIG_KEY) - ? config.getString(TRANSACTIONHISTORY_SWITCH_CONFIG_KEY) - : DEFAULT_TRANSACTIONHISTORY_SWITCH; - } - - public static int getCheckpointVersionFromConfig(final Config config) { - return config.hasPath(CHECKPOINT_VERSION_KEY) - ? config.getInt(CHECKPOINT_VERSION_KEY) - : DEFAULT_CHECKPOINT_VERSION; - } - - public static boolean getCheckpointSyncFromConfig(final Config config) { - return config.hasPath(CHECKPOINT_SYNC_KEY) - ? config.getBoolean(CHECKPOINT_SYNC_KEY) - : DEFAULT_CHECKPOINT_SYNC; - } - - public static int getEstimatedTransactionsFromConfig(final Config config) { - if (!config.hasPath(ESTIMATED_TRANSACTIONS_CONFIG_KEY)) { - return DEFAULT_ESTIMATED_TRANSACTIONS; - } - int estimatedTransactions = config.getInt(ESTIMATED_TRANSACTIONS_CONFIG_KEY); - if (estimatedTransactions > 10000) { - estimatedTransactions = 10000; - } else if (estimatedTransactions < 100) { - estimatedTransactions = 100; - } - return estimatedTransactions; - } - - public static boolean getTxCacheInitOptimizationFromConfig(final Config config) { - return config.hasPath(TX_CACHE_INIT_OPTIMIZATION) - && config.getBoolean(TX_CACHE_INIT_OPTIMIZATION); - } - - - public void setCacheStrategies(Config config) { - if (config.hasPath(CACHE_STRATEGIES)) { - config.getConfig(CACHE_STRATEGIES).resolve().entrySet().forEach(c -> + /** + * Accepts raw storage Config sub-tree because cache.strategies has dynamic keys + * (CacheType enum names) that ConfigBeanFactory cannot bind to fixed bean fields. + */ + public void setCacheStrategies(Config storageSection) { + if (storageSection.hasPath("cache.strategies")) { + storageSection.getConfig("cache.strategies").resolve().entrySet().forEach(c -> this.cacheStrategies.put(CacheType.valueOf(c.getKey()), c.getValue().unwrapped().toString())); } @@ -271,138 +143,75 @@ public Sha256Hash getDbRoot(String dbName, Sha256Hash defaultV) { return this.dbRoots.getOrDefault(dbName, defaultV); } - public void setDbRoots(Config config) { - if (config.hasPath(MERKLE_ROOT)) { - config.getConfig(MERKLE_ROOT).resolve().entrySet().forEach(c -> - this.dbRoots.put(c.getKey(), Sha256Hash.wrap( + /** + * Accepts raw storage Config sub-tree because merkleRoot has dynamic keys + * (database names) that ConfigBeanFactory cannot bind to fixed bean fields. + */ + public void setDbRoots(Config storageSection) { + if (storageSection.hasPath("merkleRoot")) { + storageSection.getConfig("merkleRoot").resolve().entrySet().forEach(c -> + this.dbRoots.put(c.getKey(), Sha256Hash.wrap( ByteString.fromHex(c.getValue().unwrapped().toString())))); } } - private Property createProperty(final ConfigObject conf) { - + /** + * Create Property from StorageConfig.PropertyConfig bean. + */ + private Property createPropertyFromBean(StorageConfig.PropertyConfig pc) { Property property = new Property(); - // Database name must be set - if (!conf.containsKey(NAME_CONFIG_KEY)) { + if (pc.getName().isEmpty()) { throw new IllegalArgumentException("[storage.properties] database name must be set."); } - property.setName(conf.get(NAME_CONFIG_KEY).unwrapped().toString()); - - // Check writable permission of path - if (conf.containsKey(PATH_CONFIG_KEY)) { - String path = conf.get(PATH_CONFIG_KEY).unwrapped().toString(); + property.setName(pc.getName()); + if (!pc.getPath().isEmpty()) { + String path = pc.getPath(); File file = new File(path); if (!file.exists() && !file.mkdirs()) { throw new IllegalArgumentException( String.format("[storage.properties] can not create storage path: %s", path)); } - if (!file.canWrite()) { throw new IllegalArgumentException( String.format("[storage.properties] permission denied to write to: %s ", path)); } - property.setPath(path); } - // Check, get and set fields of Options Options dbOptions = newDefaultDbOptions(property.getName()); - - setIfNeeded(conf, dbOptions); - + applyPropertyOptions(pc, dbOptions); property.setDbOptions(dbOptions); return property; } - private static void setIfNeeded(ConfigObject conf, Options dbOptions) { - if (conf.containsKey(CREATE_IF_MISSING_CONFIG_KEY)) { - dbOptions.createIfMissing( - Boolean.parseBoolean( - conf.get(CREATE_IF_MISSING_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(PARANOID_CHECKS_CONFIG_KEY)) { - dbOptions.paranoidChecks( - Boolean.parseBoolean( - conf.get(PARANOID_CHECKS_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(VERITY_CHECK_SUMS_CONFIG_KEY)) { - dbOptions.verifyChecksums( - Boolean.parseBoolean( - conf.get(VERITY_CHECK_SUMS_CONFIG_KEY).unwrapped().toString() - ) - ); - } - - if (conf.containsKey(COMPRESSION_TYPE_CONFIG_KEY)) { - String param = conf.get(COMPRESSION_TYPE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.compressionType( - CompressionType.getCompressionTypeByPersistentId(Integer.parseInt(param))); - } catch (NumberFormatException e) { - throwIllegalArgumentException(COMPRESSION_TYPE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(BLOCK_SIZE_CONFIG_KEY)) { - String param = conf.get(BLOCK_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.blockSize(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(BLOCK_SIZE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(WRITE_BUFFER_SIZE_CONFIG_KEY)) { - String param = conf.get(WRITE_BUFFER_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.writeBufferSize(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(WRITE_BUFFER_SIZE_CONFIG_KEY, Integer.class, param); - } - } - - if (conf.containsKey(CACHE_SIZE_CONFIG_KEY)) { - String param = conf.get(CACHE_SIZE_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.cacheSize(Long.parseLong(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(CACHE_SIZE_CONFIG_KEY, Long.class, param); - } - } - - if (conf.containsKey(MAX_OPEN_FILES_CONFIG_KEY)) { - String param = conf.get(MAX_OPEN_FILES_CONFIG_KEY).unwrapped().toString(); - try { - dbOptions.maxOpenFiles(Integer.parseInt(param)); - } catch (NumberFormatException e) { - throwIllegalArgumentException(MAX_OPEN_FILES_CONFIG_KEY, Integer.class, param); - } - } + /** + * 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()); } - private static void throwIllegalArgumentException(String param, Class type, String actual) { - throw new IllegalArgumentException( - String.format("[storage.properties] %s must be %s type, actual: %s.", - param, type.getSimpleName(), actual)); - } /** - * Set propertyMap of Storage object from Config - * - * @param config Config object from "config.conf" file + * Set propertyMap of Storage object from Config via StorageConfig bean. */ - public void setPropertyMapFromConfig(final Config config) { - if (config.hasPath(PROPERTIES_CONFIG_KEY)) { - propertyMap = config.getObjectList(PROPERTIES_CONFIG_KEY).stream() - .map(this::createProperty) + /** + * Set propertyMap from StorageConfig bean list. No Config parameter needed. + */ + public void setPropertyMapFromBean(List props) { + if (props != null && !props.isEmpty()) { + propertyMap = props.stream() + .map(this::createPropertyFromBean) .collect(Collectors.toMap(Property::getName, p -> p)); } } @@ -423,30 +232,60 @@ public void deleteAllStoragePaths() { } } - public void setDefaultDbOptions(final Config config) { + /** + * Initialize default LevelDB options and store optional per-tier overrides + * from StorageConfig bean (no raw Config needed). + */ + public void setDefaultDbOptions(StorageConfig sc) { this.defaultDbOptions = DbOptionalsUtils.createDefaultDbOptions(); - storage = config.getConfig(PROPERTIES_CONFIG_DB_KEY); + this.defaultDbOption = sc.getDefaultDbOption(); + this.defaultMDbOption = sc.getDefaultMDbOption(); + this.defaultLDbOption = sc.getDefaultLDbOption(); } - public Options newDefaultDbOptions(String name ) { - // first fetch origin default - Options options = DbOptionalsUtils.newDefaultDbOptions(name, this.defaultDbOptions); + public Options newDefaultDbOptions(String name) { + Options options = DbOptionalsUtils.newDefaultDbOptions(name, this.defaultDbOptions); - // then fetch from config for default - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_KEY)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_KEY), options); + if (defaultDbOption != null) { + applyDbOptionOverride(defaultDbOption, options); } - - // check if has middle config - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_M_KEY) && DbOptionalsUtils.DB_M.contains(name)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_M_KEY), options); - + if (defaultMDbOption != null && DbOptionalsUtils.DB_M.contains(name)) { + applyDbOptionOverride(defaultMDbOption, options); } - // check if has large config - if (storage.hasPath(PROPERTIES_CONFIG_DEFAULT_L_KEY) && DbOptionalsUtils.DB_L.contains(name)) { - setIfNeeded(storage.getObject(PROPERTIES_CONFIG_DEFAULT_L_KEY), options); + if (defaultLDbOption != null && DbOptionalsUtils.DB_L.contains(name)) { + applyDbOptionOverride(defaultLDbOption, options); } return options; } + + // 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()); + } + if (o.getWriteBufferSize() != null) { + dbOptions.writeBufferSize(o.getWriteBufferSize()); + } + if (o.getCacheSize() != null) { + dbOptions.cacheSize(o.getCacheSize()); + } + if (o.getMaxOpenFiles() != null) { + dbOptions.maxOpenFiles(o.getMaxOpenFiles()); + } + } } 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 new file mode 100644 index 00000000000..2517f4d10d7 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -0,0 +1,311 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import com.typesafe.config.ConfigObject; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.tron.common.math.StrictMathWrapper; + +/** + * Storage configuration bean. + * Field names match config.conf keys under the "storage" section. + * Covers db, index, properties, dbSettings, backup, checkpoint, txCache, etc. + */ +@Slf4j +@Getter +@Setter +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(); + private BackupConfig backup = new BackupConfig(); + private BalanceConfig balance = new BalanceConfig(); + private CheckpointConfig checkpoint = new CheckpointConfig(); + private SnapshotConfig snapshot = new SnapshotConfig(); + private TxCacheConfig txCache = new TxCacheConfig(); + private List properties = new ArrayList<>(); + + // merkleRoot is a nested object (e.g. { reward-vi = "hash..." }) not a string. + // Excluded from auto-binding, handled by Storage class directly. + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private Object merkleRoot; + + // 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(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 + @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 DbSettingsConfig { + private int levelNumber = 7; + private int compactThreads = 0; // 0 = auto: max(availableProcessors, 1) + private int blocksize = 16; + private long maxBytesForLevelBase = 256; + private double maxBytesForLevelMultiplier = 10; + private int level0FileNumCompactionTrigger = 2; + private long targetFileSizeBase = 64; + private int targetFileSizeMultiplier = 1; + private int maxOpenFiles = 5000; + + // Expand 0 → auto-detected processor count. Mirrors develop Args.java:1609-1611. + void postProcess() { + if (compactThreads == 0) { + compactThreads = StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1); + } + } + } + + @Getter + @Setter + public static class BackupConfig { + private boolean enable = false; + private String propPath = "prop.properties"; + private String bak1path = "bak1/database/"; + private String bak2path = "bak2/database/"; + private int frequency = 10000; + } + + @Getter + @Setter + public static class BalanceConfig { + private HistoryConfig history = new HistoryConfig(); + + @Getter + @Setter + public static class HistoryConfig { + private boolean lookup = false; + } + } + + @Getter + @Setter + public static class CheckpointConfig { + private int version = 1; + private boolean sync = true; + } + + @Getter + @Setter + public static class SnapshotConfig { + private int maxFlushCount = 1; + + // Reject out-of-range values. Mirrors develop Storage.getSnapshotMaxFlushCountFromConfig. + void postProcess() { + if (maxFlushCount <= 0) { + throw new IllegalArgumentException("MaxFlushCount value can not be negative or zero!"); + } + if (maxFlushCount > 500) { + throw new IllegalArgumentException("MaxFlushCount value must not exceed 500!"); + } + } + } + + @Getter + @Setter + public static class TxCacheConfig { + private int estimatedTransactions = 1000; + private boolean initOptimization = false; + + // Clamp to [100, 10000]. Mirrors develop Storage.getEstimatedTransactionsFromConfig. + void postProcess() { + if (estimatedTransactions > 10000) { + estimatedTransactions = 10000; + } else if (estimatedTransactions < 100) { + estimatedTransactions = 100; + } + } + } + + @Getter + @Setter + public static class PropertyConfig { + 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) + + public static StorageConfig fromConfig(Config config) { + Config section = config.getConfig("storage"); + + StorageConfig sc = ConfigBeanFactory.create(section, StorageConfig.class); + sc.rawStorageConfig = section; + + // Read optional LevelDB option overrides (default, defaultM, defaultL). + sc.defaultDbOption = readDbOption(section, "default"); + sc.defaultMDbOption = readDbOption(section, "defaultM"); + sc.defaultLDbOption = readDbOption(section, "defaultL"); + + sc.dbSettings.postProcess(); + sc.snapshot.postProcess(); + sc.txCache.postProcess(); + return sc; + } + + // Partial LevelDB option override for default/defaultM/defaultL. + // Uses boxed types so null means "not set by user, keep existing value". + @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); + } + } + if (conf.containsKey("blockSize")) { + String param = conf.get("blockSize").unwrapped().toString(); + try { + o.setBlockSize(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("blockSize", Integer.class, param); + } + } + if (conf.containsKey("writeBufferSize")) { + String param = conf.get("writeBufferSize").unwrapped().toString(); + try { + o.setWriteBufferSize(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("writeBufferSize", Integer.class, param); + } + } + if (conf.containsKey("cacheSize")) { + String param = conf.get("cacheSize").unwrapped().toString(); + try { + o.setCacheSize(Long.parseLong(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("cacheSize", Long.class, param); + } + } + if (conf.containsKey("maxOpenFiles")) { + String param = conf.get("maxOpenFiles").unwrapped().toString(); + try { + o.setMaxOpenFiles(Integer.parseInt(param)); + } catch (NumberFormatException e) { + throwIllegalArgumentException("maxOpenFiles", Integer.class, param); + } + } + return o; + } + + private static void throwIllegalArgumentException(String param, Class type, String actual) { + throw new IllegalArgumentException( + String.format("[storage.properties] %s must be %s type, actual: %s.", + param, type.getSimpleName(), actual)); + } +} 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 new file mode 100644 index 00000000000..d583cf4c601 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -0,0 +1,64 @@ +package org.tron.core.config.args; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigBeanFactory; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * VM configuration bean. Field names match config.conf keys under the "vm" section. + * Bound automatically via ConfigBeanFactory — no manual key constants needed. + */ +@Slf4j +@Getter +@Setter +public class VmConfig { + + private boolean supportConstant = false; + private long maxEnergyLimitForConstant = 100_000_000L; + private int lruCacheSize = 500; + private double minTimeRatio = 0.0; + private double maxTimeRatio = 5.0; + private int longRunningTime = 10; + private boolean estimateEnergy = false; + private int estimateEnergyMaxRetry = 3; + private boolean vmTrace = false; + private boolean saveInternalTx = false; + private boolean saveFeaturedInternalTx = false; + private boolean saveCancelAllUnfreezeV2Details = false; + + /** + * Create VmConfig from the "vm" section of the application config. + * Defaults come from reference.conf (loaded globally via Configuration.java), + * so no per-bean DEFAULTS needed. + */ + public static VmConfig fromConfig(Config config) { + Config vmSection = config.getConfig("vm"); + VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); + vmConfig.postProcess(); + return vmConfig; + } + + private void postProcess() { + // clamp maxEnergyLimitForConstant + if (maxEnergyLimitForConstant < 3_000_000L) { + maxEnergyLimitForConstant = 3_000_000L; + } + + // clamp estimateEnergyMaxRetry to 0-10 + if (estimateEnergyMaxRetry < 0) { + estimateEnergyMaxRetry = 0; + } + if (estimateEnergyMaxRetry > 10) { + estimateEnergyMaxRetry = 10; + } + + // cross-field dependency warning + if (saveCancelAllUnfreezeV2Details + && (!saveInternalTx || !saveFeaturedInternalTx)) { + logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as " + + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); + } + } +} diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf new file mode 100644 index 00000000000..1cfde3e5c19 --- /dev/null +++ b/common/src/main/resources/reference.conf @@ -0,0 +1,841 @@ +# ============================================================================= +# reference.conf — Full default configuration for java-tron +# ============================================================================= +# +# This file defines the default value for every configuration parameter. +# It is packaged inside the jar and loaded automatically via Typesafe Config's +# standard mechanism: ConfigFactory.defaultReference(). +# +# Loading priority (highest wins): +# 1. User's external config file (e.g. config.conf passed via -c flag) +# 2. This file (reference.conf, bundled in jar) +# +# When a user's config.conf omits a parameter, the value from this file is +# used as the fallback. This ensures the node always has a complete and valid +# configuration, even if the user only overrides a few parameters. +# +# Maintenance rules: +# - Every parameter that the code reads must have an entry here +# - Values must match the bean field initializers in the corresponding +# XxxConfig.java classes (VmConfig, NodeConfig, CommitteeConfig, etc.) +# - Keep the section order and key order identical to config.conf for +# easy side-by-side comparison +# - When adding a new parameter: add it here AND in the bean class +# +# 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 { + # type is deprecated and has no effect. + # type = mainnet +} + +storage { + # Database engine: "LEVELDB" or "ROCKSDB" (ARM only supports ROCKSDB) + db.engine = "LEVELDB" + 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" + + # 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 + # default = { + # maxOpenFiles = 100 + # } + # defaultM = { + # maxOpenFiles = 500 + # } + # defaultL = { + # maxOpenFiles = 1000 + # } + # setting can improve leveldb performance .... end, deprecated for arm + + # Example per-database overrides: + # { + # 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 + + # RocksDB settings (only used when db.engine = "ROCKSDB") + # Strongly recommend NOT modifying unless you know every item's meaning clearly. + dbSettings = { + levelNumber = 7 + compactThreads = 0 // 0 = auto: max(availableProcessors, 1) + blocksize = 16 // n * KB + maxBytesForLevelBase = 256 // n * MB + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 2 + targetFileSizeBase = 64 // n * MB + targetFileSizeMultiplier = 1 + maxOpenFiles = 5000 + } + + balance.history.lookup = false + + # Checkpoint version for snapshot mechanism. Version 2 enables V2 snapshot. + checkpoint.version = 1 + checkpoint.sync = true + + # Estimated number of block transactions (default 1000, min 100, max 10000). + # Total cached transactions = 65536 * txCache.estimatedTransactions + txCache.estimatedTransactions = 1000 + # If true, transaction cache initialization will be faster. + txCache.initOptimization = false + + # Number of blocks flushed to db in each batch during node syncing. + snapshot.maxFlushCount = 1 + + # Database backup settings (RocksDB only) + backup = { + enable = false + propPath = "prop.properties" + bak1path = "bak1/database/" + bak2path = "bak2/database/" + frequency = 10000 + } + + # Data root setting, for check data, currently only reward-vi is used. + # merkleRoot = { + # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net + # } +} + +node.discovery = { + enable = false + persist = false + external.ip = "" +} + +# 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 { + port = 10001 + priority = 0 + keepAliveInterval = 3000 + members = [ + # "ip", + # "ip" + ] +} + +# Algorithm for generating public key from private key. Do not modify to avoid forks. +crypto { + engine = "eckey" +} + +# Energy limit block number (config key has typo "enery" preserved for backward compatibility) +enery.limit.block.num = 4727890 + +# Actuator whitelist — empty means all actuators allowed +actuator { + whitelist = [] +} + +node.metrics = { + prometheus { + enable = false + port = 9527 + } + + storageEnable = false + + influxdb { + ip = "" + port = 8086 + database = "metrics" + metricsReportInterval = 10 + } +} + +node { + # Trust node for solidity node (example: "127.0.0.1:50051"). + # Empty string here = "not configured"; Args.java bridge converts "" → null so the + # runtime behavior matches develop (trustNodeAddr is null unless user sets the key). + trustNode = "" + + # Expose extension api to public or not + walletExtensionApi = false + + listen.port = 18888 + connection.timeout = 2 + fetchBlock.timeout = 500 + + # Number of blocks to fetch in one batch during sync. Range: [100, 2000]. + syncFetchBatchNum = 2000 + + # Number of validate sign threads, default availableProcessors + # Number of validate sign threads, 0 = auto (availableProcessors) + validateSignThreadNum = 0 + + maxConnections = 30 + minConnections = 8 + minActiveConnections = 3 + maxConnectionsWithSameIp = 2 + maxHttpConnectNumber = 50 + minParticipationRate = 0 + + # Whether to enable shielded transaction API + allowShieldedTransactionApi = true + + # Whether to print config log at startup + openPrintLog = true + + # If true, SR packs transactions into a block in descending order of fee; + # otherwise, packs by receive timestamp. + openTransactionSort = false + + # Threshold for broadcast transactions received from each peer per second, + # transactions exceeding this are discarded + maxTps = 1000 + + isOpenFullTcpDisconnect = false + inactiveThreshold = 600 // seconds + tcpNettyWorkThreadNum = 0 + udpNettyWorkThreadNum = 1 + 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`. + channel.read.timeout = 0 + metricsEnable = false + + p2p { + version = 11111 # Mainnet:11111; Nile:201910292; Shasta:1 + } + + active = [ + # Active establish connection in any case + # "ip:port", + # "ip:port" + ] + + passive = [ + # Passive accept connection in any case + # "ip:port", + # "ip:port" + ] + + fastForward = [ + "100.27.171.62:18888", + "15.188.6.125:18888" + ] + + http { + fullNodeEnable = true + fullNodePort = 8090 + solidityEnable = true + solidityPort = 8091 + PBFTEnable = true + PBFTPort = 8092 + + # Maximum HTTP request body size, default 4MB. Independent from rpc.maxMessageSize. + maxMessageSize = 4M + } + + rpc { + enable = true + port = 50051 + solidityEnable = true + solidityPort = 50061 + PBFTEnable = true + PBFTPort = 50071 + + # Number of gRPC threads, 0 = auto (availableProcessors / 2) + thread = 0 + + # Maximum concurrent calls per incoming connection + # No limit on concurrent calls per connection + maxConcurrentCallsPerConnection = 2147483647 + + # HTTP/2 flow control window (bytes), default 1MB + flowControlWindow = 1048576 + + # Connection idle timeout (ms). No limit by default. + maxConnectionIdleInMillis = 9223372036854775807 + + # Connection max age (ms). No limit by default. + maxConnectionAgeInMillis = 9223372036854775807 + + # Maximum message size (bytes), default 4MB + maxMessageSize = 4194304 + + # Maximum header list size (bytes), default 8192 + maxHeaderListSize = 8192 + + # RST_STREAM frames allowed per connection per period, 0 = no limit + maxRstStream = 0 + + # Seconds per period for gRPC RST_STREAM limit + secondsPerWindow = 0 + + # Minimum effective connections required to broadcast transactions + minEffectiveConnection = 1 + + # Reflection service switch for grpcurl tool + reflectionService = false + trxCacheEnable = false + } + + # Number of solidity threads in FullNode. + # Increase if solidity rpc/http interface timeouts occur. + # Default: number of cpu cores. + # Number of solidity threads, 0 = auto (availableProcessors) + solidity.threads = 0 + + # Maximum percentage of producing block interval (provides time for broadcast etc.) + blockProducedTimeOut = 50 + + # Maximum transactions from network layer per second + netMaxTrxPerSecond = 700 + + # Whether to enable node detection function + nodeDetectEnable = false + + # Use IPv6 address for node discovery and TCP connection + enableIpv6 = false + + # If node's highest block is below all peers, try to acquire new connection + effectiveCheckEnable = false + + # Dynamic loading configuration function + dynamicConfig = { + enable = false + checkInterval = 600 + } + + # Block solidification check + unsolidifiedBlockCheck = false + maxUnsolidifiedBlocks = 54 + blockCacheTimeout = 60 + + # TCP and transaction limits + receiveTcpMinDataLength = 2048 + maxTransactionPendingSize = 2000 + pendingTransactionTimeout = 60000 + + # Consensus agreement + agreeNodeCount = 0 + + # Shielded transaction (ZK) + zenTokenId = "000000" + shieldedTransInPendingMaxCounts = 10 + + # Contract proto validation thread pool (0 = auto: availableProcessors) + validContractProto.threads = 0 + + dns { + treeUrls = [ + # "tree://AKMQMNAJJBL73LXWPXDI4I5ZWWIZ4AWO34DWQ636QOBBXNFXH3LQS@main.trondisco.net", + ] + publish = false + dnsDomain = "" + dnsPrivate = "" + knownUrls = [] + staticNodes = [] + maxMergeSize = 0 + changeThreshold = 0.0 + serverType = "" + accessKeyId = "" + accessKeySecret = "" + aliyunDnsEndpoint = "" + awsRegion = "" + awsHostZoneId = "" + } + + # Open history query APIs on lite FullNode (may return null for some queries) + openHistoryQueryWhenLiteFN = false + + jsonrpc { + httpFullNodeEnable = false + httpFullNodePort = 8545 + httpSolidityEnable = false + httpSolidityPort = 8555 + httpPBFTEnable = false + httpPBFTPort = 8565 + + # Maximum blocks range for eth_getLogs, >0 otherwise no limit + maxBlockRange = 5000 + + # Maximum topics within a topic criteria, >0 otherwise no limit + maxSubTopics = 1000 + + # Maximum number for blockFilter + maxBlockFilterNum = 50000 + + # Maximum JSON-RPC request body size, default 4MB. Independent from rpc.maxMessageSize. + maxMessageSize = 4M + } + + # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. + disabledApi = [ + # "getaccount", + # "getnowblock2" + ] +} + +## Rate limiter config +rate.limiter = { + # Strategies: GlobalPreemptibleAdapter, QpsRateLimiterAdapter, IPQPSRateLimiterAdapter + # Default: QpsRateLimiterAdapter with qps=1000 + + 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 = { + syncBlockChain = 3.0 + fetchInvData = 3.0 + disconnect = 1.0 + } + + global.qps = 50000 + global.ip.qps = 10000 + global.api.qps = 1000 +} + +seed.node = { + ip.list = [ + "3.225.171.164:18888", + "52.8.46.215:18888", + "3.79.71.167:18888", + "108.128.110.16:18888", + "18.133.82.227:18888", + "35.180.81.133:18888", + "13.210.151.5:18888", + "18.231.27.82:18888", + "3.12.212.122:18888", + "52.24.128.7:18888", + "15.207.144.3:18888", + "3.39.38.55:18888", + "54.151.226.240:18888", + "35.174.93.198:18888", + "18.210.241.149:18888", + "54.177.115.127:18888", + "54.254.131.82:18888", + "18.167.171.167:18888", + "54.167.11.177:18888", + "35.74.7.196:18888", + "52.196.244.176:18888", + "54.248.129.19:18888", + "43.198.142.160:18888", + "3.0.214.7:18888", + "54.153.59.116:18888", + "54.153.94.160:18888", + "54.82.161.39:18888", + "54.179.207.68:18888", + "18.142.82.44:18888", + "18.163.230.203:18888", + # "[2a05:d014:1f2f:2600:1b15:921:d60b:4c60]:18888", // use this if support ipv6 + # "[2600:1f18:7260:f400:8947:ebf3:78a0:282b]:18888", // use this if support ipv6 + ] +} + +genesis.block = { + assets = [ + { + accountName = "Zion" + accountType = "AssetIssue" + address = "TLLM21wteSPs4hKjbxgmH1L6poyMjeTbHm" + balance = "99000000000000000" + }, + { + accountName = "Sun" + accountType = "AssetIssue" + address = "TXmVpin5vq5gdZsciyyjdZgKRUju4st1wM" + balance = "0" + }, + { + accountName = "Blackhole" + accountType = "AssetIssue" + address = "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" + balance = "-9223372036854775808" + } + ] + + witnesses = [ + { + address: THKJYuUmMKKARNf7s2VT51g5uPY6KEqnat, + url = "http://GR1.com", + voteCount = 100000026 + }, + { + address: TVDmPWGYxgi5DNeW8hXrzrhY8Y6zgxPNg4, + url = "http://GR2.com", + voteCount = 100000025 + }, + { + address: TWKZN1JJPFydd5rMgMCV5aZTSiwmoksSZv, + url = "http://GR3.com", + voteCount = 100000024 + }, + { + address: TDarXEG2rAD57oa7JTK785Yb2Et32UzY32, + url = "http://GR4.com", + voteCount = 100000023 + }, + { + address: TAmFfS4Tmm8yKeoqZN8x51ASwdQBdnVizt, + url = "http://GR5.com", + voteCount = 100000022 + }, + { + address: TK6V5Pw2UWQWpySnZyCDZaAvu1y48oRgXN, + url = "http://GR6.com", + voteCount = 100000021 + }, + { + address: TGqFJPFiEqdZx52ZR4QcKHz4Zr3QXA24VL, + url = "http://GR7.com", + voteCount = 100000020 + }, + { + address: TC1ZCj9Ne3j5v3TLx5ZCDLD55MU9g3XqQW, + url = "http://GR8.com", + voteCount = 100000019 + }, + { + address: TWm3id3mrQ42guf7c4oVpYExyTYnEGy3JL, + url = "http://GR9.com", + voteCount = 100000018 + }, + { + address: TCvwc3FV3ssq2rD82rMmjhT4PVXYTsFcKV, + url = "http://GR10.com", + voteCount = 100000017 + }, + { + address: TFuC2Qge4GxA2U9abKxk1pw3YZvGM5XRir, + url = "http://GR11.com", + voteCount = 100000016 + }, + { + address: TNGoca1VHC6Y5Jd2B1VFpFEhizVk92Rz85, + url = "http://GR12.com", + voteCount = 100000015 + }, + { + address: TLCjmH6SqGK8twZ9XrBDWpBbfyvEXihhNS, + url = "http://GR13.com", + voteCount = 100000014 + }, + { + address: TEEzguTtCihbRPfjf1CvW8Euxz1kKuvtR9, + url = "http://GR14.com", + voteCount = 100000013 + }, + { + address: TZHvwiw9cehbMxrtTbmAexm9oPo4eFFvLS, + url = "http://GR15.com", + voteCount = 100000012 + }, + { + address: TGK6iAKgBmHeQyp5hn3imB71EDnFPkXiPR, + url = "http://GR16.com", + voteCount = 100000011 + }, + { + address: TLaqfGrxZ3dykAFps7M2B4gETTX1yixPgN, + url = "http://GR17.com", + voteCount = 100000010 + }, + { + address: TX3ZceVew6yLC5hWTXnjrUFtiFfUDGKGty, + url = "http://GR18.com", + voteCount = 100000009 + }, + { + address: TYednHaV9zXpnPchSywVpnseQxY9Pxw4do, + url = "http://GR19.com", + voteCount = 100000008 + }, + { + address: TCf5cqLffPccEY7hcsabiFnMfdipfyryvr, + url = "http://GR20.com", + voteCount = 100000007 + }, + { + address: TAa14iLEKPAetX49mzaxZmH6saRxcX7dT5, + url = "http://GR21.com", + voteCount = 100000006 + }, + { + address: TBYsHxDmFaRmfCF3jZNmgeJE8sDnTNKHbz, + url = "http://GR22.com", + voteCount = 100000005 + }, + { + address: TEVAq8dmSQyTYK7uP1ZnZpa6MBVR83GsV6, + url = "http://GR23.com", + voteCount = 100000004 + }, + { + address: TRKJzrZxN34YyB8aBqqPDt7g4fv6sieemz, + url = "http://GR24.com", + voteCount = 100000003 + }, + { + address: TRMP6SKeFUt5NtMLzJv8kdpYuHRnEGjGfe, + url = "http://GR25.com", + voteCount = 100000002 + }, + { + address: TDbNE1VajxjpgM5p7FyGNDASt3UVoFbiD3, + url = "http://GR26.com", + voteCount = 100000001 + }, + { + address: TLTDZBcPoJ8tZ6TTEeEqEvwYFk2wgotSfD, + url = "http://GR27.com", + voteCount = 100000000 + } + ] + + timestamp = "0" + + parentHash = "0xe58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f" +} + +# Optional. Used when the witness account has set witnessPermission. +# localWitnessAccountAddress = + +localwitness = [ +] + +# localwitnesskeystore = [ +# "localwitnesskeystore.json" +# ] + +block = { + needSyncCheck = false + maintenanceTimeInterval = 21600000 // 6 hours (ms) + proposalExpireTime = 259200000 // 3 days (ms), controlled by committee proposal + checkFrozenTime = 1 // maintenance periods to check frozen balance (test only) +} + +# Transaction reference block: "solid" or "head". Default "solid". "head" may cause TaPos error. +trx.reference.block = "solid" + +# Transaction expiration time in milliseconds. +trx.expiration.timeInMilliseconds = 60000 + +vm = { + supportConstant = false + maxEnergyLimitForConstant = 100000000 + minTimeRatio = 0.0 + maxTimeRatio = 5.0 + saveInternalTx = false + lruCacheSize = 500 + vmTrace = false + + # Whether to store featured internal transactions (freeze, vote, etc.) + saveFeaturedInternalTx = false + + # Whether to store details of CANCELALLUNFREEZEV2 opcode internal transactions + saveCancelAllUnfreezeV2Details = false + + # Max execution time (ms) for re-executed transactions during packaging + longRunningTime = 10 + + # Whether to support estimate energy API + estimateEnergy = false + + # Max retry time for executing transaction in estimating energy + estimateEnergyMaxRetry = 3 +} + +# Governance proposal toggle parameters. All default to 0 (disabled). +# Controlled by on-chain committee proposals, not manual configuration. +# Setting them in config is only for private chain testing. +committee = { + allowCreationOfContracts = 0 + allowMultiSign = 0 + allowAdaptiveEnergy = 0 + allowDelegateResource = 0 + allowSameTokenName = 0 + allowTvmTransferTrc10 = 0 + allowTvmConstantinople = 0 + allowTvmSolidity059 = 0 + forbidTransferToContract = 0 + allowShieldedTRC20Transaction = 0 + allowTvmIstanbul = 0 + allowMarketTransaction = 0 + allowProtoFilterNum = 0 + allowAccountStateRoot = 0 + changedDelegation = 0 + allowPBFT = 0 + pBFTExpireNum = 20 + allowTransactionFeePool = 0 + allowBlackHoleOptimization = 0 + allowNewResourceModel = 0 + allowReceiptsMerkleRoot = 0 + allowTvmFreeze = 0 + allowTvmVote = 0 + unfreezeDelayDays = 0 + allowTvmLondon = 0 + allowTvmCompatibleEvm = 0 + allowHigherLimitForMaxCpuTimeOfOneTx = 0 + allowNewRewardAlgorithm = 0 + allowOptimizedReturnValueOfChainId = 0 + allowTvmShangHai = 0 + allowOldRewardOpt = 0 + allowEnergyAdjustment = 0 + allowStrictMath = 0 + consensusLogicOptimization = 0 + allowTvmCancun = 0 + allowTvmBlob = 0 + allowTvmOsaka = 0 + allowAccountAssetOptimization = 0 + allowAssetOptimization = 0 + allowNewReward = 0 + memoFee = 0 + allowDelegateOptimization = 0 + allowDynamicEnergy = 0 + dynamicEnergyThreshold = 0 + dynamicEnergyIncreaseFactor = 0 + dynamicEnergyMaxFactor = 0 +} + +event.subscribe = { + enable = false + + native = { + useNativeQueue = true + bindport = 5555 + sendqueuelength = 1000 + } + + version = 0 + startSyncBlockNum = 0 + path = "" + server = "" + dbconfig = "" + contractParse = true + + topics = [ + { + triggerName = "block" + enable = false + topic = "block" + solidified = false + }, + { + triggerName = "transaction" + enable = false + topic = "transaction" + solidified = false + ethCompatible = false + }, + { + triggerName = "contractevent" + enable = false + topic = "contractevent" + }, + { + triggerName = "contractlog" + enable = false + topic = "contractlog" + redundancy = false + }, + { + triggerName = "solidity" + enable = true + topic = "solidity" + }, + { + triggerName = "solidityevent" + enable = false + topic = "solidityevent" + }, + { + triggerName = "soliditylog" + enable = false + topic = "soliditylog" + redundancy = false + } + ] + + filter = { + fromblock = "" + toblock = "" + contractAddress = [ + "" + ] + contractTopic = [ + "" + ] + } +} diff --git a/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java b/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java new file mode 100644 index 00000000000..14645242851 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/BlockConfigTest.java @@ -0,0 +1,56 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.core.exception.TronError; + +public class BlockConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + BlockConfig bc = BlockConfig.fromConfig(withRef()); + assertEquals(21600000L, bc.getMaintenanceTimeInterval()); + assertEquals(1, bc.getCheckFrozenTime()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "block { needSyncCheck = true, maintenanceTimeInterval = 10000," + + " checkFrozenTime = 5, proposalExpireTime = 300000 }"); + BlockConfig bc = BlockConfig.fromConfig(config); + assertEquals(true, bc.isNeedSyncCheck()); + assertEquals(10000L, bc.getMaintenanceTimeInterval()); + assertEquals(5, bc.getCheckFrozenTime()); + assertEquals(300000L, bc.getProposalExpireTime()); + } + + @Test(expected = TronError.class) + public void testProposalExpireTimeTooLow() { + BlockConfig.fromConfig(withRef("block { proposalExpireTime = 0 }")); + } + + @Test(expected = TronError.class) + public void testProposalExpireTimeTooHigh() { + BlockConfig.fromConfig(withRef("block { proposalExpireTime = 999999999999 }")); + } + + @Test(expected = TronError.class) + public void testRejectsCommitteeProposalExpireTime() { + BlockConfig.fromConfig(withRef( + "committee { proposalExpireTime = 300000 }\n" + + "block { proposalExpireTime = 300000 }")); + } +} 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 new file mode 100644 index 00000000000..962b6a349ab --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/CommitteeConfigTest.java @@ -0,0 +1,233 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class CommitteeConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + CommitteeConfig cc = CommitteeConfig.fromConfig(withRef()); + assertEquals(0, cc.getAllowCreationOfContracts()); + assertEquals(0, cc.getAllowPBFT()); + assertEquals(20, cc.getPBFTExpireNum()); + assertEquals(0, cc.getUnfreezeDelayDays()); + assertEquals(0, cc.getAllowDynamicEnergy()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "committee { allowCreationOfContracts = 1, allowPBFT = 1, pBFTExpireNum = 30 }"); + CommitteeConfig cc = CommitteeConfig.fromConfig(config); + assertEquals(1, cc.getAllowCreationOfContracts()); + assertEquals(1, cc.getAllowPBFT()); + assertEquals(30, cc.getPBFTExpireNum()); + } + + @Test + public void testUnfreezeDelayDaysClamped() { + assertEquals(365, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 500 }")).getUnfreezeDelayDays()); + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = -10 }")).getUnfreezeDelayDays()); + } + + @Test + public void testDynamicEnergyClamped() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowDynamicEnergy = 5 }")).getAllowDynamicEnergy()); + } + + @Test + public void testDynamicEnergyThresholdClamped() { + assertEquals(100_000_000_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyThreshold = 999999999999999999 }")) + .getDynamicEnergyThreshold()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAllowOldRewardOptWithoutPrerequisites() { + CommitteeConfig.fromConfig(withRef("committee { allowOldRewardOpt = 1 }")); + } + + @Test + public void testAllowOldRewardOptWithPrerequisite() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowTvmVote = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // + // Background: PR #6615 (config bean refactor) silently dropped clamps for + // memoFee and allowNewReward because no test covered the boundary cases. + // These tests pin every clamp in CommitteeConfig.postProcess() so future + // refactors cannot drop them undetected. + // =========================================================================== + + // ----- memoFee: clamped to [0, 1_000_000_000] ----- + + @Test + public void testMemoFeeClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { memoFee = -100 }")).getMemoFee()); + } + + @Test + public void testMemoFeeClampedAboveMax() { + assertEquals(1_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 5000000000 }")).getMemoFee()); + } + + @Test + public void testMemoFeeInRangeUnchanged() { + assertEquals(500_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 500000000 }")).getMemoFee()); + } + + @Test + public void testMemoFeeBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 0 }")).getMemoFee()); + assertEquals(1_000_000_000L, CommitteeConfig.fromConfig( + withRef("committee { memoFee = 1000000000 }")).getMemoFee()); + } + + // ----- allowNewReward: clamped to [0, 1] ----- + + @Test + public void testAllowNewRewardClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = -5 }")).getAllowNewReward()); + } + + @Test + public void testAllowNewRewardClampedAboveOne() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 99 }")).getAllowNewReward()); + } + + @Test + public void testAllowNewRewardBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 0 }")).getAllowNewReward()); + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowNewReward = 1 }")).getAllowNewReward()); + } + + // Critical: clamp must run BEFORE the cross-field check, otherwise + // `allowNewReward = 2` (intended as "enabled") would still satisfy + // `allowNewReward != 1` and the cross-field check would throw. + // This test pins the clamp ordering. + @Test + public void testAllowNewRewardClampRunsBeforeCrossFieldCheck() { + CommitteeConfig cc = CommitteeConfig.fromConfig(withRef( + "committee { allowOldRewardOpt = 1, allowNewReward = 2 }")); + assertEquals(1, cc.getAllowNewReward()); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + // ----- allowDelegateOptimization: clamped to [0, 1] ----- + + @Test + public void testAllowDelegateOptimizationClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowDelegateOptimization = -3 }")) + .getAllowDelegateOptimization()); + } + + @Test + public void testAllowDelegateOptimizationClampedAboveOne() { + assertEquals(1, CommitteeConfig.fromConfig( + withRef("committee { allowDelegateOptimization = 7 }")) + .getAllowDelegateOptimization()); + } + + // ----- allowDynamicEnergy: clamped to [0, 1] ----- + + @Test + public void testAllowDynamicEnergyClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { allowDynamicEnergy = -1 }")).getAllowDynamicEnergy()); + } + + // ----- unfreezeDelayDays: clamped to [0, 365] (boundary values) ----- + + @Test + public void testUnfreezeDelayDaysBoundaryValues() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 0 }")).getUnfreezeDelayDays()); + assertEquals(365, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 365 }")).getUnfreezeDelayDays()); + assertEquals(100, CommitteeConfig.fromConfig( + withRef("committee { unfreezeDelayDays = 100 }")).getUnfreezeDelayDays()); + } + + // ----- dynamicEnergyThreshold: clamped to [0, 100_000_000_000_000_000] ----- + + @Test + public void testDynamicEnergyThresholdClampedBelowZero() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyThreshold = -1 }")) + .getDynamicEnergyThreshold()); + } + + // ----- dynamicEnergyIncreaseFactor: clamped to [0, 10_000] ----- + + @Test + public void testDynamicEnergyIncreaseFactorClamped() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = -1 }")) + .getDynamicEnergyIncreaseFactor()); + assertEquals(10_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = 10001 }")) + .getDynamicEnergyIncreaseFactor()); + assertEquals(5_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyIncreaseFactor = 5000 }")) + .getDynamicEnergyIncreaseFactor()); + } + + // ----- dynamicEnergyMaxFactor: clamped to [0, 100_000] ----- + + @Test + public void testDynamicEnergyMaxFactorClamped() { + assertEquals(0, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = -1 }")) + .getDynamicEnergyMaxFactor()); + assertEquals(100_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = 100001 }")) + .getDynamicEnergyMaxFactor()); + assertEquals(50_000L, CommitteeConfig.fromConfig( + withRef("committee { dynamicEnergyMaxFactor = 50000 }")) + .getDynamicEnergyMaxFactor()); + } + + // ----- Cross-field validation for allowOldRewardOpt ----- + + @Test + public void testAllowOldRewardOptWithAllowNewReward() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowNewReward = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } + + @Test + public void testAllowOldRewardOptWithAllowNewRewardAlgorithm() { + CommitteeConfig cc = CommitteeConfig.fromConfig( + withRef("committee { allowOldRewardOpt = 1, allowNewRewardAlgorithm = 1 }")); + assertEquals(1, cc.getAllowOldRewardOpt()); + } +} 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 new file mode 100644 index 00000000000..361d9f48581 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/EventConfigTest.java @@ -0,0 +1,82 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class EventConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + EventConfig ec = EventConfig.fromConfig(empty); + // reference.conf has event.subscribe with enable=false, topics with 7 entries + assertFalse(ec.isEnable()); + assertEquals(0, ec.getVersion()); + assertEquals("", ec.getPath()); + assertFalse(ec.getTopics().isEmpty()); // reference.conf has default topic entries + } + + @Test + public void testNativeQueue() { + Config config = withRef( + "event.subscribe { enable = true," + + " native { useNativeQueue = true, bindport = 6666, sendqueuelength = 2000 } }"); + EventConfig ec = EventConfig.fromConfig(config); + assertTrue(ec.isEnable()); + assertTrue(ec.getNativeQueue().isUseNativeQueue()); + assertEquals(6666, ec.getNativeQueue().getBindport()); + assertEquals(2000, ec.getNativeQueue().getSendqueuelength()); + } + + @Test + public void testTopicsWithOptionalFields() { + Config config = withRef( + "event.subscribe { enable = true, topics = [" + + "{ triggerName = block, enable = true, topic = block }," + + "{ triggerName = transaction, enable = false, topic = tx," + + " ethCompatible = true, solidified = true, redundancy = true }" + + "] }"); + EventConfig ec = EventConfig.fromConfig(config); + assertEquals(2, ec.getTopics().size()); + + EventConfig.TopicConfig t1 = ec.getTopics().get(0); + assertEquals("block", t1.getTriggerName()); + assertTrue(t1.isEnable()); + assertFalse(t1.isEthCompatible()); // not set, default false + assertFalse(t1.isSolidified()); + assertFalse(t1.isRedundancy()); + + EventConfig.TopicConfig t2 = ec.getTopics().get(1); + assertEquals("transaction", t2.getTriggerName()); + assertTrue(t2.isEthCompatible()); + assertTrue(t2.isSolidified()); + assertTrue(t2.isRedundancy()); + } + + @Test + public void testFilter() { + Config config = withRef( + "event.subscribe { enable = true," + + " filter { fromblock = \"100\", toblock = \"200\"," + + " contractAddress = [\"addr1\", \"addr2\"]," + + " contractTopic = [\"topic1\"] } }"); + EventConfig ec = EventConfig.fromConfig(config); + assertEquals("100", ec.getFilter().getFromblock()); + assertEquals("200", ec.getFilter().getToblock()); + assertEquals(2, ec.getFilter().getContractAddress().size()); + assertEquals(1, ec.getFilter().getContractTopic().size()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java new file mode 100644 index 00000000000..5e653a79b7f --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/GenesisConfigTest.java @@ -0,0 +1,59 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class GenesisConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + GenesisConfig gc = GenesisConfig.fromConfig(empty); + // reference.conf has genesis.block with timestamp, parentHash, assets, witnesses + assertEquals("0", gc.getTimestamp()); + assertFalse(gc.getAssets().isEmpty()); // reference.conf has seed accounts + assertFalse(gc.getWitnesses().isEmpty()); // reference.conf has seed witnesses + } + + @Test + public void testWithAssets() { + Config config = withRef( + "genesis.block { timestamp = \"12345\", parentHash = \"0x00\"," + + " assets = [{ accountName = Zion, accountType = AssetIssue," + + " address = \"TAddr1\", balance = \"99000\" }]," + + " witnesses = [{ address = \"TWitness1\", url = \"http://test.com\"," + + " voteCount = 100 }] }"); + GenesisConfig gc = GenesisConfig.fromConfig(config); + assertEquals("12345", gc.getTimestamp()); + assertEquals("0x00", gc.getParentHash()); + assertEquals(1, gc.getAssets().size()); + assertEquals("Zion", gc.getAssets().get(0).getAccountName()); + assertEquals("TAddr1", gc.getAssets().get(0).getAddress()); + assertEquals(1, gc.getWitnesses().size()); + assertEquals("TWitness1", gc.getWitnesses().get(0).getAddress()); + assertEquals(100, gc.getWitnesses().get(0).getVoteCount()); + } + + @Test + public void testEmptyLists() { + Config config = withRef( + "genesis.block { timestamp = \"0\", parentHash = \"0x00\"," + + " assets = [], witnesses = [] }"); + GenesisConfig gc = GenesisConfig.fromConfig(config); + assertTrue(gc.getAssets().isEmpty()); + assertTrue(gc.getWitnesses().isEmpty()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java b/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java new file mode 100644 index 00000000000..0c163ef31f7 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/LocalWitnessConfigTest.java @@ -0,0 +1,48 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class LocalWitnessConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(empty); + assertTrue(lw.getPrivateKeys().isEmpty()); + assertNull(lw.getAccountAddress()); + assertTrue(lw.getKeystores().isEmpty()); + } + + @Test + public void testWithPrivateKeys() { + Config config = withRef( + "localwitness = [\"key1\", \"key2\"]\n" + + "localWitnessAccountAddress = \"TAddr123\""); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(config); + assertEquals(2, lw.getPrivateKeys().size()); + assertEquals("key1", lw.getPrivateKeys().get(0)); + assertEquals("TAddr123", lw.getAccountAddress()); + } + + @Test + public void testWithKeystores() { + Config config = withRef( + "localwitnesskeystore = [\"/path/to/keystore1\"]"); + LocalWitnessConfig lw = LocalWitnessConfig.fromConfig(config); + assertEquals(1, lw.getKeystores().size()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java b/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java new file mode 100644 index 00000000000..b641e4d1924 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/MetricsConfigTest.java @@ -0,0 +1,48 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class MetricsConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + MetricsConfig mc = MetricsConfig.fromConfig(empty); + assertFalse(mc.isStorageEnable()); + assertFalse(mc.getPrometheus().isEnable()); + assertEquals(9527, mc.getPrometheus().getPort()); + assertEquals(8086, mc.getInfluxdb().getPort()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "node.metrics {" + + " storageEnable = true," + + " prometheus { enable = true, port = 9999 }," + + " influxdb { ip = \"10.0.0.1\", port = 9086, database = mydb," + + " metricsReportInterval = 30 } }"); + MetricsConfig mc = MetricsConfig.fromConfig(config); + assertTrue(mc.isStorageEnable()); + assertTrue(mc.getPrometheus().isEnable()); + assertEquals(9999, mc.getPrometheus().getPort()); + assertEquals("10.0.0.1", mc.getInfluxdb().getIp()); + assertEquals(9086, mc.getInfluxdb().getPort()); + assertEquals("mydb", mc.getInfluxdb().getDatabase()); + assertEquals(30, mc.getInfluxdb().getMetricsReportInterval()); + } +} diff --git a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java new file mode 100644 index 00000000000..ed369d6c35f --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java @@ -0,0 +1,56 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.core.Constant; + +public class MiscConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + MiscConfig mc = MiscConfig.fromConfig(empty); + assertTrue(mc.isNeedToUpdateAsset()); + assertFalse(mc.isHistoryBalanceLookup()); + assertEquals("solid", mc.getTrxReferenceBlock()); + assertEquals(Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME, + mc.getTrxExpirationTimeInMilliseconds()); + // reference.conf has crypto.engine = "eckey" (lowercase) + assertEquals("eckey", mc.getCryptoEngine()); + // reference.conf has seed.node.ip.list with actual IPs + assertFalse(mc.getSeedNodeIpList().isEmpty()); + assertTrue(mc.getActuatorWhitelist().isEmpty()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "storage { needToUpdateAsset = false," + + " balance { history { lookup = true } } }\n" + + "trx { reference { block = head } }\n" + + "crypto { engine = sm2 }\n" + + "seed.node { ip.list = [\"1.2.3.4:18888\"] }\n" + + "actuator { whitelist = [\"CreateSmartContract\"] }"); + MiscConfig mc = MiscConfig.fromConfig(config); + assertFalse(mc.isNeedToUpdateAsset()); + assertTrue(mc.isHistoryBalanceLookup()); + assertEquals("head", mc.getTrxReferenceBlock()); + assertEquals("sm2", mc.getCryptoEngine()); + assertEquals(1, mc.getSeedNodeIpList().size()); + assertEquals(1, mc.getActuatorWhitelist().size()); + assertTrue(mc.getActuatorWhitelist().contains("CreateSmartContract")); + } +} 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 new file mode 100644 index 00000000000..fb22029262e --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -0,0 +1,319 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class NodeConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + NodeConfig nc = NodeConfig.fromConfig(empty); + assertEquals(18888, nc.getListenPort()); + assertEquals(2, nc.getConnectionTimeout()); + assertEquals(500, nc.getFetchBlockTimeout()); + assertEquals(30, nc.getMaxConnections()); + assertEquals(8, nc.getMinConnections()); + assertEquals(4, nc.getMaxFastForwardNum()); + assertFalse(nc.isOpenFullTcpDisconnect()); + // reference.conf matches code default: discovery disabled when not configured + assertFalse(nc.isDiscoveryEnable()); + assertFalse(nc.isDiscoveryPersist()); + assertEquals(0, nc.getChannelReadTimeout()); + } + + @Test + public void testDotNotationFields() { + Config config = withRef( + "node { listen { port = 19999 }, connection { timeout = 5 }," + + " fetchBlock { timeout = 300 }, solidity { threads = 4 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(19999, nc.getListenPort()); + assertEquals(5, nc.getConnectionTimeout()); + assertEquals(300, nc.getFetchBlockTimeout()); + assertEquals(4, nc.getSolidityThreads()); + } + + @Test + public void testDiscoveryFields() { + Config config = withRef( + "node.discovery { enable = true, persist = true }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertTrue(nc.isDiscoveryEnable()); + assertTrue(nc.isDiscoveryPersist()); + } + + @Test + public void testHttpSubBean() { + Config config = withRef( + "node { http { fullNodeEnable = false, fullNodePort = 9090," + + " PBFTEnable = false, PBFTPort = 9092 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertFalse(nc.getHttp().isFullNodeEnable()); + assertEquals(9090, nc.getHttp().getFullNodePort()); + assertFalse(nc.getHttp().isPBFTEnable()); + assertEquals(9092, nc.getHttp().getPBFTPort()); + } + + @Test + public void testRpcSubBean() { + Config config = withRef( + "node { rpc { enable = false, port = 60051," + + " PBFTEnable = false, PBFTPort = 60071 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertFalse(nc.getRpc().isEnable()); + assertEquals(60051, nc.getRpc().getPort()); + assertFalse(nc.getRpc().isPBFTEnable()); + assertEquals(60071, nc.getRpc().getPBFTPort()); + } + + @Test + public void testBackupSubBean() { + Config config = withRef( + "node { backup { priority = 5, port = 20001, keepAliveInterval = 5000 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(5, nc.getBackup().getPriority()); + assertEquals(20001, nc.getBackup().getPort()); + assertEquals(5000, nc.getBackup().getKeepAliveInterval()); + } + + @Test + public void testIsOpenFullTcpDisconnect() { + Config config = withRef( + "node { isOpenFullTcpDisconnect = true }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertTrue(nc.isOpenFullTcpDisconnect()); + } + + @Test + public void testRpcDefaultsFromReference() { + Config empty = withRef(); + NodeConfig nc = NodeConfig.fromConfig(empty); + NodeConfig.RpcConfig rpc = nc.getRpc(); + + // reference.conf provides actual final defaults, no sentinel conversion needed + assertEquals(2147483647, rpc.getMaxConcurrentCallsPerConnection()); + assertEquals(1048576, rpc.getFlowControlWindow()); + assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis()); + assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis()); + assertEquals(4194304, rpc.getMaxMessageSize()); + assertEquals(8192, rpc.getMaxHeaderListSize()); + assertEquals(1, rpc.getMinEffectiveConnection()); + // thread=0 in reference.conf triggers auto-detect in postProcess + assertTrue(rpc.getThread() > 0); + } + + @Test + public void testRpcUserOverrideZeroNotConverted() { + // Users can explicitly set 0 to disable connection checks (e.g. system-test) + Config config = withRef( + "node { rpc { minEffectiveConnection = 0 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(0, nc.getRpc().getMinEffectiveConnection()); + } + + @Test + public void testRpcUserOverrideExplicitValues() { + Config config = withRef( + "node { rpc { thread = 32," + + " maxConcurrentCallsPerConnection = 50," + + " flowControlWindow = 2097152," + + " maxMessageSize = 8388608," + + " maxHeaderListSize = 16384 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + NodeConfig.RpcConfig rpc = nc.getRpc(); + assertEquals(32, rpc.getThread()); + assertEquals(50, rpc.getMaxConcurrentCallsPerConnection()); + assertEquals(2097152, rpc.getFlowControlWindow()); + assertEquals(8388608, rpc.getMaxMessageSize()); + assertEquals(16384, rpc.getMaxHeaderListSize()); + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // Pin every clamp in NodeConfig.postProcess() so future refactors cannot + // drop them undetected (regression seen in PR #6615 with CommitteeConfig). + // =========================================================================== + + // ----- blockProducedTimeOut: clamped to [30, 100] ----- + + @Test + public void testBlockProducedTimeOutClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 10 }")); + assertEquals(30, nc.getBlockProducedTimeOut()); + } + + @Test + public void testBlockProducedTimeOutClampedAboveMax() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 200 }")); + assertEquals(100, nc.getBlockProducedTimeOut()); + } + + @Test + public void testBlockProducedTimeOutBoundaryValues() { + assertEquals(30, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 30 }")).getBlockProducedTimeOut()); + assertEquals(100, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 100 }")).getBlockProducedTimeOut()); + assertEquals(75, NodeConfig.fromConfig( + withRef("node { blockProducedTimeOut = 75 }")).getBlockProducedTimeOut()); + } + + // ----- inactiveThreshold: minimum 1 ----- + + @Test + public void testInactiveThresholdClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 0 }")); + assertEquals(1, nc.getInactiveThreshold()); + } + + @Test + public void testInactiveThresholdClampedNegative() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { inactiveThreshold = -100 }")); + assertEquals(1, nc.getInactiveThreshold()); + } + + @Test + public void testInactiveThresholdInRangeUnchanged() { + assertEquals(1, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 1 }")).getInactiveThreshold()); + assertEquals(600, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 600 }")).getInactiveThreshold()); + assertEquals(1000, NodeConfig.fromConfig( + withRef("node { inactiveThreshold = 1000 }")).getInactiveThreshold()); + } + + // ----- maxFastForwardNum: clamped to [1, MAX_ACTIVE_WITNESS_NUM=27] ----- + + @Test + public void testMaxFastForwardNumClampedBelowMin() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 0 }")); + assertEquals(1, nc.getMaxFastForwardNum()); + } + + @Test + public void testMaxFastForwardNumClampedAboveMax() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 100 }")); + assertEquals(27, nc.getMaxFastForwardNum()); + } + + @Test + public void testMaxFastForwardNumBoundaryValues() { + assertEquals(1, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 1 }")).getMaxFastForwardNum()); + assertEquals(27, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 27 }")).getMaxFastForwardNum()); + assertEquals(4, NodeConfig.fromConfig( + withRef("node { maxFastForwardNum = 4 }")).getMaxFastForwardNum()); + } + + // ----- validContractProto.threads: 0 = auto (availableProcessors) ----- + + @Test + public void testValidContractProtoThreadsDefaultAutoExpands() { + // Default in reference.conf is 0; postProcess must expand to availableProcessors. + // Matches develop Args.java:743-746 runtime fallback. + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertEquals(Runtime.getRuntime().availableProcessors(), + nc.getValidContractProtoThreads()); + } + + @Test + public void testValidContractProtoThreadsExplicitPreserved() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { validContractProto { threads = 3 } }")); + assertEquals(3, nc.getValidContractProtoThreads()); + } + + // ----- trustNode: empty reference.conf default means trustNode stays unset ----- + + @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()); + } + + // ----- maxConnectionsWithSameIp alias: reference.conf must not poison merge ----- + + @Test + public void testMaxConnectionsWithSameIpNotOverriddenByReferenceConfAlias() { + // reference.conf must NOT ship `maxActiveNodesWithSameIp`, otherwise the alias- + // fallback branch would silently clobber the user's modern key. Regression guard + // for review #2 (317787106, 2026-04-16). + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxConnectionsWithSameIp = 10 }")); + assertEquals(10, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testMaxActiveNodesWithSameIpLegacyAliasStillWorks() { + // Back-compat: users who still write the legacy key in their config.conf + // must get their value routed to maxConnectionsWithSameIp. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxActiveNodesWithSameIp = 5 }")); + assertEquals(5, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testLegacyAliasTakesPriorityOverModernKey() { + // Matches develop Args.java:392-399: if the legacy key is present, it wins. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node { maxActiveNodesWithSameIp = 5, maxConnectionsWithSameIp = 10 }")); + assertEquals(5, nc.getMaxConnectionsWithSameIp()); + } + + @Test + public void testShieldedApiDefaultsToTrueWhenNeitherKeySet() { + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertTrue(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiModernKeyRespected() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node.allowShieldedTransactionApi = false")); + assertFalse(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiLegacyKeyRespected() { + // Regression guard: reference.conf ships `allowShieldedTransactionApi = true`, which + // used to make the legacy-key fallback dead code. A user who only set the legacy key + // must still have their value honored. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node.fullNodeAllowShieldedTransaction = false")); + assertFalse(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiLegacyKeyTakesPriorityOverModern() { + // Consistent with maxActiveNodesWithSameIp: legacy key presence wins over modern. + NodeConfig nc = NodeConfig.fromConfig( + withRef("node {\n" + + " allowShieldedTransactionApi = false\n" + + " fullNodeAllowShieldedTransaction = true\n" + + "}")); + assertTrue(nc.isAllowShieldedTransactionApi()); + } +} 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 new file mode 100644 index 00000000000..7b4d8a87d45 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/RateLimiterConfigTest.java @@ -0,0 +1,54 @@ +package org.tron.core.config.args; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class RateLimiterConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + RateLimiterConfig rl = RateLimiterConfig.fromConfig(empty); + assertEquals(50000, rl.getGlobal().getQps()); + assertEquals(10000, rl.getGlobal().getIp().getQps()); + assertEquals(1000, rl.getGlobal().getApi().getQps()); + assertEquals(3.0, rl.getP2p().getSyncBlockChain(), 0.001); + assertEquals(3.0, rl.getP2p().getFetchInvData(), 0.001); + assertEquals(1.0, rl.getP2p().getDisconnect(), 0.001); + assertTrue(rl.getHttp().isEmpty()); + assertTrue(rl.getRpc().isEmpty()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "rate.limiter {" + + " global { qps = 100, ip { qps = 50 }, api { qps = 10 } }," + + " p2p { syncBlockChain = 5.0, disconnect = 2.0 }," + + " http = [{ component = TestServlet, strategy = QpsRateLimiterAdapter," + + " paramString = \"qps=10\" }]," + + " rpc = [{ component = TestRpc, strategy = GlobalPreemptibleAdapter," + + " paramString = \"permit=1\" }]" + + "}"); + RateLimiterConfig rl = RateLimiterConfig.fromConfig(config); + assertEquals(100, rl.getGlobal().getQps()); + assertEquals(50, rl.getGlobal().getIp().getQps()); + assertEquals(5.0, rl.getP2p().getSyncBlockChain(), 0.001); + assertEquals(1, rl.getHttp().size()); + assertEquals("TestServlet", rl.getHttp().get(0).getComponent()); + assertEquals(1, rl.getRpc().size()); + assertEquals("TestRpc", rl.getRpc().get(0).getComponent()); + } +} 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 new file mode 100644 index 00000000000..5a679be89e5 --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -0,0 +1,142 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; +import org.tron.common.math.StrictMathWrapper; + +public class StorageConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + assertEquals("LEVELDB", sc.getDb().getEngine()); + assertFalse(sc.getDb().isSync()); + assertEquals("database", sc.getDb().getDirectory()); + assertEquals("index", sc.getIndex().getDirectory()); + assertTrue(sc.isNeedToUpdateAsset()); + assertFalse(sc.getBackup().isEnable()); + assertEquals(10000, sc.getBackup().getFrequency()); + assertEquals(7, sc.getDbSettings().getLevelNumber()); + assertEquals(5000, sc.getDbSettings().getMaxOpenFiles()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "storage { db { engine = ROCKSDB, sync = true, directory = mydb }," + + " backup { enable = true, frequency = 5000 }," + + " dbSettings { levelNumber = 5, maxOpenFiles = 3000 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals("ROCKSDB", sc.getDb().getEngine()); + assertTrue(sc.getDb().isSync()); + assertEquals("mydb", sc.getDb().getDirectory()); + assertTrue(sc.getBackup().isEnable()); + assertEquals(5000, sc.getBackup().getFrequency()); + assertEquals(5, sc.getDbSettings().getLevelNumber()); + assertEquals(3000, sc.getDbSettings().getMaxOpenFiles()); + } + + @Test + public void testCheckpointDefaults() { + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + assertEquals(1, sc.getCheckpoint().getVersion()); + assertTrue(sc.getCheckpoint().isSync()); + } + + @Test + public void testDbSettingsDefaults() { + // These defaults must match develop's Args.initRocksDbSettings() fallbacks so that + // nodes with minimal configs retain the same RocksDB tuning. See + // docs/plans/2026-04-21-001-fix-reference-conf-default-drift.md. + Config empty = withRef(); + StorageConfig sc = StorageConfig.fromConfig(empty); + StorageConfig.DbSettingsConfig ds = sc.getDbSettings(); + assertEquals(7, ds.getLevelNumber()); + // compactThreads default is 0 in reference.conf, auto-expanded by postProcess() + assertEquals(StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1), + ds.getCompactThreads()); + assertEquals(16, ds.getBlocksize()); + assertEquals(256, ds.getMaxBytesForLevelBase()); + assertEquals(10, ds.getMaxBytesForLevelMultiplier(), 0.01); + assertEquals(2, ds.getLevel0FileNumCompactionTrigger()); + assertEquals(64, ds.getTargetFileSizeBase()); + assertEquals(1, ds.getTargetFileSizeMultiplier()); + assertEquals(5000, ds.getMaxOpenFiles()); + } + + @Test + public void testCompactThreadsAutoExpand() { + // compactThreads = 0 must be auto-expanded to availableProcessors (min 1) + Config config = withRef("storage { dbSettings { compactThreads = 0 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals(StrictMathWrapper.max(Runtime.getRuntime().availableProcessors(), 1), + sc.getDbSettings().getCompactThreads()); + } + + @Test + public void testCompactThreadsExplicitPreserved() { + // Non-zero compactThreads must be passed through untouched + Config config = withRef("storage { dbSettings { compactThreads = 7 } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertEquals(7, sc.getDbSettings().getCompactThreads()); + } + + @Test + public void testBalanceHistoryLookup() { + Config config = withRef( + "storage { balance { history { lookup = true } } }"); + StorageConfig sc = StorageConfig.fromConfig(config); + assertTrue(sc.getBalance().getHistory().isLookup()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountZeroRejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = 0")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountNegativeRejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = -1")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSnapshotMaxFlushCountOver500Rejected() { + StorageConfig.fromConfig(withRef("storage.snapshot.maxFlushCount = 501")); + } + + @Test + public void testTxCacheEstimatedClampedBelowMin() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 50")); + assertEquals(100, sc.getTxCache().getEstimatedTransactions()); + } + + @Test + public void testTxCacheEstimatedClampedAboveMax() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 99999")); + assertEquals(10000, sc.getTxCache().getEstimatedTransactions()); + } + + @Test + public void testTxCacheEstimatedWithinRangePreserved() { + StorageConfig sc = StorageConfig.fromConfig( + withRef("storage.txCache.estimatedTransactions = 5000")); + assertEquals(5000, sc.getTxCache().getEstimatedTransactions()); + } +} 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 new file mode 100644 index 00000000000..b134fe00c2b --- /dev/null +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -0,0 +1,91 @@ +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; +import com.typesafe.config.ConfigFactory; +import org.junit.Test; + +public class VmConfigTest { + + private static Config withRef(String hocon) { + return ConfigFactory.parseString(hocon).withFallback(ConfigFactory.defaultReference()); + } + + private static Config withRef() { + return ConfigFactory.defaultReference(); + } + + @Test + public void testDefaults() { + Config empty = withRef(); + VmConfig vm = VmConfig.fromConfig(empty); + assertFalse(vm.isSupportConstant()); + assertEquals(100_000_000L, vm.getMaxEnergyLimitForConstant()); + assertEquals(500, vm.getLruCacheSize()); + assertEquals(0.0, vm.getMinTimeRatio(), 0.001); + assertEquals(5.0, vm.getMaxTimeRatio(), 0.001); + assertEquals(10, vm.getLongRunningTime()); + assertFalse(vm.isEstimateEnergy()); + assertEquals(3, vm.getEstimateEnergyMaxRetry()); + assertFalse(vm.isVmTrace()); + assertFalse(vm.isSaveInternalTx()); + assertFalse(vm.isSaveFeaturedInternalTx()); + assertFalse(vm.isSaveCancelAllUnfreezeV2Details()); + } + + @Test + public void testFromConfig() { + Config config = withRef( + "vm { supportConstant = true, lruCacheSize = 1000, minTimeRatio = 0.5 }"); + VmConfig vm = VmConfig.fromConfig(config); + assertTrue(vm.isSupportConstant()); + assertEquals(1000, vm.getLruCacheSize()); + assertEquals(0.5, vm.getMinTimeRatio(), 0.001); + } + + @Test + public void testMaxEnergyLimitClamped() { + Config config = withRef("vm { maxEnergyLimitForConstant = 100 }"); + VmConfig vm = VmConfig.fromConfig(config); + assertEquals(3_000_000L, vm.getMaxEnergyLimitForConstant()); + } + + @Test + public void testEstimateEnergyMaxRetryClamped() { + Config tooHigh = withRef("vm { estimateEnergyMaxRetry = 50 }"); + assertEquals(10, VmConfig.fromConfig(tooHigh).getEstimateEnergyMaxRetry()); + + Config tooLow = withRef("vm { estimateEnergyMaxRetry = -5 }"); + assertEquals(0, VmConfig.fromConfig(tooLow).getEstimateEnergyMaxRetry()); + } + + @Test + public void testPartialConfig() { + Config config = withRef("vm { saveInternalTx = true }"); + VmConfig vm = VmConfig.fromConfig(config); + assertTrue(vm.isSaveInternalTx()); + assertFalse(vm.isSupportConstant()); // default + assertEquals(500, vm.getLruCacheSize()); // default + } + + // =========================================================================== + // Boundary tests for postProcess() clamps + // Pin every clamp in VmConfig.postProcess() so future refactors cannot + // drop them undetected (regression seen in PR #6615 with CommitteeConfig). + // =========================================================================== + + // ----- estimateEnergyMaxRetry: clamped to [0, 10] ----- + + @Test + public void testEstimateEnergyMaxRetryBoundaryValues() { + assertEquals(0, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 0 }")).getEstimateEnergyMaxRetry()); + assertEquals(10, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 10 }")).getEstimateEnergyMaxRetry()); + assertEquals(3, VmConfig.fromConfig( + withRef("vm { estimateEnergyMaxRetry = 3 }")).getEstimateEnergyMaxRetry()); + } +} diff --git a/framework/src/main/java/org/tron/keystore/Credentials.java b/crypto/src/main/java/org/tron/keystore/Credentials.java similarity index 100% rename from framework/src/main/java/org/tron/keystore/Credentials.java rename to crypto/src/main/java/org/tron/keystore/Credentials.java diff --git a/framework/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java similarity index 74% rename from framework/src/main/java/org/tron/keystore/Wallet.java rename to crypto/src/main/java/org/tron/keystore/Wallet.java index d38b1c74984..d63525b1e4d 100644 --- a/framework/src/main/java/org/tron/keystore/Wallet.java +++ b/crypto/src/main/java/org/tron/keystore/Wallet.java @@ -23,7 +23,6 @@ import org.tron.common.crypto.SignUtils; import org.tron.common.utils.ByteArray; import org.tron.common.utils.StringUtil; -import org.tron.core.config.args.Args; import org.tron.core.exception.CipherException; /** @@ -48,7 +47,12 @@ */ public class Wallet { - protected static final String AES_128_CTR = "pbkdf2"; + // KDF identifiers used in the Web3 Secret Storage "kdf" field. + // The old name "AES_128_CTR" was misleading — the value is the PBKDF2 KDF + // identifier, not the cipher (CIPHER below). The inner class name + // `WalletFile.Aes128CtrKdfParams` is kept for wire-format/Jackson-subtype + // backward compatibility even though it also reflects the same history. + protected static final String PBKDF2 = "pbkdf2"; protected static final String SCRYPT = "scrypt"; private static final int N_LIGHT = 1 << 12; private static final int P_LIGHT = 6; @@ -168,8 +172,8 @@ private static byte[] generateMac(byte[] derivedKey, byte[] cipherText) { return Hash.sha3(result); } - public static SignInterface decrypt(String password, WalletFile walletFile) - throws CipherException { + public static SignInterface decrypt(String password, WalletFile walletFile, + boolean ecKey) throws CipherException { validate(walletFile); @@ -205,32 +209,79 @@ public static SignInterface decrypt(String password, WalletFile walletFile) byte[] derivedMac = generateMac(derivedKey, cipherText); - if (!Arrays.equals(derivedMac, mac)) { + if (!java.security.MessageDigest.isEqual(derivedMac, mac)) { throw new CipherException("Invalid password provided"); } byte[] encryptKey = Arrays.copyOfRange(derivedKey, 0, 16); byte[] privateKey = performCipherOperation(Cipher.DECRYPT_MODE, iv, encryptKey, cipherText); - return SignUtils.fromPrivate(privateKey, Args.getInstance().isECKeyCryptoEngine()); - } + SignInterface keyPair = SignUtils.fromPrivate(privateKey, ecKey); + + // Enforce address consistency: if the keystore declares an address, it MUST match + // the address derived from the decrypted private key. Prevents address spoofing + // where a crafted keystore displays one address but encrypts a different key. + String declared = walletFile.getAddress(); + if (declared != null && !declared.isEmpty()) { + String derived = StringUtil.encode58Check(keyPair.getAddress()); + if (!declared.equals(derived)) { + throw new CipherException( + "Keystore address mismatch: file declares " + declared + + " but private key derives " + derived); + } + } - static void validate(WalletFile walletFile) throws CipherException { - WalletFile.Crypto crypto = walletFile.getCrypto(); + return keyPair; + } + /** + * Returns a description of the first schema violation found in + * {@code walletFile}, or {@code null} if the file matches the supported + * V3 keystore shape (current version, known cipher, known KDF). + * + *

Shared by {@link #validate(WalletFile)} (which throws the message) + * and {@link #isValidKeystoreFile(WalletFile)} (which returns boolean + * for discovery-style filtering). + */ + private static String validationError(WalletFile walletFile) { if (walletFile.getVersion() != CURRENT_VERSION) { - throw new CipherException("Wallet version is not supported"); + return "Wallet version is not supported"; } - - if (!crypto.getCipher().equals(CIPHER)) { - throw new CipherException("Wallet cipher is not supported"); + WalletFile.Crypto crypto = walletFile.getCrypto(); + if (crypto == null) { + return "Missing crypto section"; + } + String cipher = crypto.getCipher(); + if (cipher == null || !cipher.equals(CIPHER)) { + return "Wallet cipher is not supported"; } + String kdf = crypto.getKdf(); + if (kdf == null || (!kdf.equals(PBKDF2) && !kdf.equals(SCRYPT))) { + return "KDF type is not supported"; + } + return null; + } - if (!crypto.getKdf().equals(AES_128_CTR) && !crypto.getKdf().equals(SCRYPT)) { - throw new CipherException("KDF type is not supported"); + static void validate(WalletFile walletFile) throws CipherException { + String error = validationError(walletFile); + if (error != null) { + throw new CipherException(error); } } + /** + * Returns {@code true} iff {@code walletFile} has the shape of a + * decryptable V3 keystore: non-null address, supported version, non-null + * crypto section with a supported cipher and KDF. Intended for + * discovery-style filtering (e.g. listing or duplicate detection) where + * we want to skip JSON stubs that would later fail {@link #validate}. + */ + public static boolean isValidKeystoreFile(WalletFile walletFile) { + return walletFile != null + && walletFile.getAddress() != null + && validationError(walletFile) == null; + } + public static byte[] generateRandomBytes(int size) { byte[] bytes = new byte[size]; new SecureRandom().nextBytes(bytes); diff --git a/framework/src/main/java/org/tron/keystore/WalletFile.java b/crypto/src/main/java/org/tron/keystore/WalletFile.java similarity index 99% rename from framework/src/main/java/org/tron/keystore/WalletFile.java rename to crypto/src/main/java/org/tron/keystore/WalletFile.java index 1f5135fefd3..97e538d1a8a 100644 --- a/framework/src/main/java/org/tron/keystore/WalletFile.java +++ b/crypto/src/main/java/org/tron/keystore/WalletFile.java @@ -165,7 +165,7 @@ public KdfParams getKdfparams() { include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "kdf") @JsonSubTypes({ - @JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.AES_128_CTR), + @JsonSubTypes.Type(value = Aes128CtrKdfParams.class, name = Wallet.PBKDF2), @JsonSubTypes.Type(value = ScryptKdfParams.class, name = Wallet.SCRYPT) }) // To support my Ether Wallet keys uncomment this annotation & comment out the above diff --git a/crypto/src/main/java/org/tron/keystore/WalletUtils.java b/crypto/src/main/java/org/tron/keystore/WalletUtils.java new file mode 100644 index 00000000000..2ce100823d9 --- /dev/null +++ b/crypto/src/main/java/org/tron/keystore/WalletUtils.java @@ -0,0 +1,267 @@ +package org.tron.keystore; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Scanner; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.tron.common.crypto.SignInterface; +import org.tron.core.exception.CipherException; + +/** + * Utility functions for working with Wallet files. + */ +@Slf4j(topic = "keystore") +public class WalletUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final Set OWNER_ONLY = + Collections.unmodifiableSet(EnumSet.of( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + + static { + objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public static String generateWalletFile( + String password, SignInterface ecKeyPair, File destinationDirectory, boolean useFullScrypt) + throws CipherException, IOException { + + WalletFile walletFile; + if (useFullScrypt) { + walletFile = Wallet.createStandard(password, ecKeyPair); + } else { + walletFile = Wallet.createLight(password, ecKeyPair); + } + + String fileName = getWalletFileName(walletFile); + File destination = new File(destinationDirectory, fileName); + writeWalletFile(walletFile, destination); + + return fileName; + } + + /** + * Write a WalletFile to the given destination path with owner-only (0600) + * permissions, using a temp file + atomic rename. + * + *

On POSIX filesystems, the temp file is created atomically with 0600 + * permissions via {@link Files#createTempFile(Path, String, String, + * java.nio.file.attribute.FileAttribute[])}, avoiding any window where the + * file is world-readable. + * + *

On non-POSIX filesystems (e.g. Windows) the fallback uses + * {@link File#setReadable(boolean, boolean)} / + * {@link File#setWritable(boolean, boolean)} which is best-effort — these + * methods manipulate only DOS-style attributes on Windows and may not update + * file ACLs. The sensitive keystore JSON is written only after the narrowing + * calls, so no confidential data is exposed during the window, but callers + * on Windows should not infer strict owner-only ACL enforcement from this. + * + * @param walletFile the keystore to serialize + * @param destination the final target file (existing file will be replaced) + */ + public static void writeWalletFile(WalletFile walletFile, File destination) + throws IOException { + Path dir = destination.getAbsoluteFile().getParentFile().toPath(); + Files.createDirectories(dir); + + Path tmp; + try { + tmp = Files.createTempFile(dir, "keystore-", ".tmp", + PosixFilePermissions.asFileAttribute(OWNER_ONLY)); + } catch (UnsupportedOperationException e) { + // Windows / non-POSIX fallback — best-effort narrowing only (see JavaDoc) + tmp = Files.createTempFile(dir, "keystore-", ".tmp"); + File tf = tmp.toFile(); + tf.setReadable(false, false); + tf.setReadable(true, true); + tf.setWritable(false, false); + tf.setWritable(true, true); + } + + try { + objectMapper.writeValue(tmp.toFile(), walletFile); + try { + Files.move(tmp, destination.toPath(), + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (Exception e) { + try { + Files.deleteIfExists(tmp); + } catch (IOException suppress) { + e.addSuppressed(suppress); + } + throw e; + } + } + + public static Credentials loadCredentials(String password, File source, boolean ecKey) + throws IOException, CipherException { + warnIfSymbolicLink(source); + WalletFile walletFile = objectMapper.readValue(source, WalletFile.class); + return Credentials.create(Wallet.decrypt(password, walletFile, ecKey)); + } + + /** + * Emit a warning if {@code source} is a symbolic link. The keystore is still + * read (following the symlink), preserving compatibility with legitimate + * deployments that use symlinks to organize keystore files (e.g. + * {@code /opt/tron/keystore/witness.json} -> {@code /mnt/encrypted/...}, + * container volume-mount paths). The warning gives operators a chance to + * notice if a path they did not expect to be a symlink has become one — for + * example if an attacker with config-injection ability has redirected the + * SR startup keystore. This mirrors how Ethereum consensus clients (e.g. + * Lighthouse) handle a configured {@code voting_keystore_path}. + */ + private static void warnIfSymbolicLink(File source) { + try { + BasicFileAttributes attrs = Files.readAttributes(source.toPath(), + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attrs.isSymbolicLink()) { + logger.warn("Keystore file is a symbolic link: {} — proceeding, " + + "but verify the symlink target points where you expect.", + source.getPath()); + } + } catch (IOException ignored) { + // If we can't stat, let the subsequent readValue surface the real error. + } + } + + public static String getWalletFileName(WalletFile walletFile) { + DateTimeFormatter format = DateTimeFormatter.ofPattern( + "'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'"); + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + + return now.format(format) + walletFile.getAddress() + ".json"; + } + + /** + * Strip trailing line terminators ({@code \n}/{@code \r}) and a leading + * UTF-8 BOM ({@code \uFEFF}) from a line of input. Unlike + * {@link String#trim()} this preserves internal whitespace, so passwords + * containing spaces (e.g. passphrases) survive intact. + * + *

Intended as the canonical helper for normalizing raw user-provided + * password/line input across both CLI console and file-driven paths. + * Returns {@code null} if the input is {@code null}. + */ + public static String stripPasswordLine(String s) { + if (s == null) { + return null; + } + if (s.length() > 0 && s.charAt(0) == '\uFEFF') { + s = s.substring(1); + } + int end = s.length(); + while (end > 0) { + char c = s.charAt(end - 1); + if (c == '\n' || c == '\r') { + end--; + } else { + break; + } + } + return s.substring(0, end); + } + + public static boolean passwordValid(String password) { + if (StringUtils.isEmpty(password)) { + return false; + } + if (password.length() < 6) { + return false; + } + //Other rule; + return true; + } + + /** + * Lazily-initialized Scanner shared across successive + * {@link #inputPassword()} calls on the non-TTY path so that + * {@link #inputPassword2Twice()} can read two lines in sequence + * without losing data. Each call to {@code new Scanner(System.in)} + * internally buffers bytes from the underlying {@link BufferedReader}; + * constructing a second Scanner after the first has been discarded + * drops any buffered bytes the first pulled from stdin, causing + * {@code NoSuchElementException}. + */ + private static Scanner sharedStdinScanner; + + /** + * Visible for testing: reset the cached Scanner so subsequent calls + * see a freshly rebound {@link System#in}. + */ + static synchronized void resetSharedStdinScanner() { + sharedStdinScanner = null; + } + + private static synchronized Scanner getSharedStdinScanner() { + if (sharedStdinScanner == null) { + sharedStdinScanner = new Scanner(System.in); + } + return sharedStdinScanner; + } + + public static String inputPassword() { + String password; + Console cons = System.console(); + Scanner in = cons == null ? getSharedStdinScanner() : null; + while (true) { + if (cons != null) { + char[] pwd = cons.readPassword("password: "); + password = String.valueOf(pwd); + } else { + // Preserve the full password including embedded whitespace. + // The previous implementation applied trim() + split("\\s+")[0] + // which silently truncated passwords like "correct horse battery + // staple" to "correct" when piped via stdin (e.g. echo ... | java). + // stripPasswordLine only removes the UTF-8 BOM and trailing line + // terminators — internal whitespace is part of the password. + password = stripPasswordLine(in.nextLine()); + } + if (passwordValid(password)) { + return password; + } + System.out.println("Invalid password, please input again."); + } + } + + public static String inputPassword2Twice() { + String password0; + while (true) { + System.out.println("Please input password."); + password0 = inputPassword(); + System.out.println("Please input password again."); + String password1 = inputPassword(); + if (password0.equals(password1)) { + break; + } + System.out.println("Two passwords do not match, please input again."); + } + return password0; + } +} diff --git a/docs/implement-a-customized-actuator-en.md b/docs/implement-a-customized-actuator-en.md index 551a6d63d3b..9cd5b4958db 100644 --- a/docs/implement-a-customized-actuator-en.md +++ b/docs/implement-a-customized-actuator-en.md @@ -36,13 +36,19 @@ message Transaction { AccountCreateContract = 0; TransferContract = 1; ........ - SumContract = 52; + SumContract = 60; } ... } ``` -Then register a function to ensure that gRPC can receive and identify the requests of this contract. Currently, gRPC protocols are all defined in `src/main/protos/api/api.proto`. To add an `InvokeSum` interface in Wallet Service: +Then register a function to ensure that gRPC can receive and identify the requests of this contract. Currently, gRPC protocols are all defined in `src/main/protos/api/api.proto`. First add the import for the new proto file at the top of `api.proto`: + +```protobuf +import "core/contract/math_contract.proto"; +``` + +Then add an `InvokeSum` interface in the Wallet service: ```protobuf service Wallet { @@ -60,13 +66,11 @@ service Wallet { ``` At last, recompile the modified proto files. Compiling the java-tron project directly will compile the proto files as well, `protoc` command is also supported. -*Currently, java-tron uses protoc v3.4.0. Please keep the same version when compiling by `protoc` command.* - ```shell -# recommended +# recommended — also recompiles proto files automatically ./gradlew build -x test -# or build via protoc +# or build via protoc (ensure the protoc version matches the one declared in build.gradle) protoc -I=src/main/protos -I=src/main/protos/core --java_out=src/main/java Tron.proto protoc -I=src/main/protos/core/contract --java_out=src/main/java math_contract.proto protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java_out=src/main/java api.proto @@ -78,7 +82,11 @@ After compilation, the corresponding .class under the java_out directory will be For now, the default Actuator supported by java-tron is located in `org.tron.core.actuator`. Creating `SumActuator` under this directory: +> **Note**: The Actuator must be placed in the `org.tron.core.actuator` package. At node startup, `TransactionRegister.registerActuator()` uses reflection to scan that package and auto-discovers every `AbstractActuator` subclass. Each subclass is instantiated once (triggering the `super()` constructor which calls `TransactionFactory.register()`), so no manual registration code is needed. + ```java +import static org.tron.core.config.Parameter.ChainConstant.TRANSFER_FEE; + public class SumActuator extends AbstractActuator { public SumActuator() { @@ -210,48 +218,47 @@ At last, run a test class to validate whether the above steps are correct: ```java public class SumActuatorTest { private static final Logger logger = LoggerFactory.getLogger("Test"); - private String serviceNode = "127.0.0.1:50051"; - private String confFile = "config-localtest.conf"; - private String dbPath = "output-directory"; - private TronApplicationContext context; - private Application appTest; - private ManagedChannel channelFull = null; - private WalletGrpc.WalletBlockingStub blockingStubFull = null; + private static final String SERVICE_NODE = "127.0.0.1:50051"; + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); + + private static TronApplicationContext context; + private static Application appTest; + private static ManagedChannel channelFull; + private static WalletGrpc.WalletBlockingStub blockingStubFull; /** - * init the application. + * init the application once for all tests in this class. */ - @Before - public void init() { - CommonParameter argsTest = Args.getInstance(); - Args.setParam(new String[]{"--output-directory", dbPath}, - confFile); + @BeforeClass + public static void init() throws IOException { + Args.setParam(new String[]{"--output-directory", + temporaryFolder.newFolder().toString()}, "config-localtest.conf"); context = new TronApplicationContext(DefaultConfig.class); - RpcApiService rpcApiService = context.getBean(RpcApiService.class); appTest = ApplicationFactory.create(context); - appTest.addService(rpcApiService); - appTest.initServices(argsTest); - appTest.startServices(); appTest.startup(); - channelFull = ManagedChannelBuilder.forTarget(serviceNode) + channelFull = ManagedChannelBuilder.forTarget(SERVICE_NODE) .usePlaintext() .build(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); } /** - * destroy the context. + * destroy the context after all tests finish. */ - @After - public void destroy() throws InterruptedException { + @AfterClass + public static void destroy() throws InterruptedException { if (channelFull != null) { - channelFull.shutdown().awaitTermination(5, TimeUnit.SECONDS); + channelFull.shutdown(); + channelFull.awaitTermination(5, TimeUnit.SECONDS); } - Args.clearParam(); - appTest.shutdownServices(); appTest.shutdown(); context.destroy(); - FileUtil.deleteDir(new File(dbPath)); + Args.clearParam(); } @Test diff --git a/docs/implement-a-customized-actuator-zh.md b/docs/implement-a-customized-actuator-zh.md index a03f6aeb228..1c2bcd8f082 100644 --- a/docs/implement-a-customized-actuator-zh.md +++ b/docs/implement-a-customized-actuator-zh.md @@ -38,13 +38,19 @@ message Transaction { AccountCreateContract = 0; TransferContract = 1; ........ - SumContract = 52; + SumContract = 60; } ... } ``` -然后还需要注册一个方法来保证 gRPC 能够接收并识别该类型合约的请求,目前 gRPC 协议统一定义在 src/main/protos/api/api.proto,在 api.proto 中的 Wallet Service 新增 `InvokeSum` 接口: +然后还需要注册一个方法来保证 gRPC 能够接收并识别该类型合约的请求,目前 gRPC 协议统一定义在 src/main/protos/api/api.proto。首先在 `api.proto` 顶部添加对新 proto 文件的 import: + +```protobuf +import "core/contract/math_contract.proto"; +``` + +然后在 Wallet service 中新增 `InvokeSum` 接口: ```protobuf service Wallet { @@ -62,13 +68,11 @@ service Wallet { ``` 最后重新编译修改过 proto 文件,可自行编译也可直接通过编译 java-tron 项目来编译 proto 文件: -*目前 java-tron 采用的是 protoc v3.4.0,自行编译时确保 protoc 版本一致。* - ```shell -# recommended +# 推荐方式 —— 直接编译项目,proto 文件会自动重新编译 ./gradlew build -x test -# or build via protoc +# 或者手动使用 protoc(版本需与 build.gradle 中声明的一致) protoc -I=src/main/protos -I=src/main/protos/core --java_out=src/main/java Tron.proto protoc -I=src/main/protos/core/contract --java_out=src/main/java math_contract.proto protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java_out=src/main/java api.proto @@ -80,7 +84,11 @@ protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java 目前 java-tron 默认支持的 Actuator 存放在该模块的 org.tron.core.actuator 目录下,同样在该目录下创建 `SumActuator` : +> **注意**:Actuator 必须放在 `org.tron.core.actuator` 包下。节点启动时,`TransactionRegister.registerActuator()` 会通过反射扫描该包,自动发现所有 `AbstractActuator` 的子类,并各实例化一次(触发 `super()` 构造器,进而调用 `TransactionFactory.register()`)。因此无需手动编写注册代码。 + ```java +import static org.tron.core.config.Parameter.ChainConstant.TRANSFER_FEE; + public class SumActuator extends AbstractActuator { public SumActuator() { @@ -212,48 +220,47 @@ public class WalletApi extends WalletImplBase { ```java public class SumActuatorTest { private static final Logger logger = LoggerFactory.getLogger("Test"); - private String serviceNode = "127.0.0.1:50051"; - private String confFile = "config-localtest.conf"; - private String dbPath = "output-directory"; - private TronApplicationContext context; - private Application appTest; - private ManagedChannel channelFull = null; - private WalletGrpc.WalletBlockingStub blockingStubFull = null; + private static final String SERVICE_NODE = "127.0.0.1:50051"; + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); + + private static TronApplicationContext context; + private static Application appTest; + private static ManagedChannel channelFull; + private static WalletGrpc.WalletBlockingStub blockingStubFull; /** - * init the application. + * 整个测试类只初始化一次应用上下文。 */ - @Before - public void init() { - CommonParameter argsTest = Args.getInstance(); - Args.setParam(new String[]{"--output-directory", dbPath}, - confFile); + @BeforeClass + public static void init() throws IOException { + Args.setParam(new String[]{"--output-directory", + temporaryFolder.newFolder().toString()}, "config-localtest.conf"); context = new TronApplicationContext(DefaultConfig.class); - RpcApiService rpcApiService = context.getBean(RpcApiService.class); appTest = ApplicationFactory.create(context); - appTest.addService(rpcApiService); - appTest.initServices(argsTest); - appTest.startServices(); appTest.startup(); - channelFull = ManagedChannelBuilder.forTarget(serviceNode) + channelFull = ManagedChannelBuilder.forTarget(SERVICE_NODE) .usePlaintext() .build(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); } /** - * destroy the context. + * 所有测试结束后统一销毁上下文。 */ - @After - public void destroy() throws InterruptedException { + @AfterClass + public static void destroy() throws InterruptedException { if (channelFull != null) { - channelFull.shutdown().awaitTermination(5, TimeUnit.SECONDS); + channelFull.shutdown(); + channelFull.awaitTermination(5, TimeUnit.SECONDS); } - Args.clearParam(); - appTest.shutdownServices(); appTest.shutdown(); context.destroy(); - FileUtil.deleteDir(new File(dbPath)); + Args.clearParam(); } @Test diff --git a/docs/modular-deployment-en.md b/docs/modular-deployment-en.md index ef48f54b269..c93ba6c39d8 100644 --- a/docs/modular-deployment-en.md +++ b/docs/modular-deployment-en.md @@ -1,8 +1,8 @@ # How to deploy java-tron after modularization -After modularization, java-tron is launched via shell script instead of typing command: `java -jar FullNode.jar`. +After modularization, the recommended way to launch java-tron is via the shell script generated in `bin/`. The classic `java -jar FullNode.jar` command is still fully supported as an alternative. -*`java -jar FullNode.jar` still works, but will be deprecated in future*. +> **Supported platforms**: Linux and macOS. Windows is not supported. ## Download @@ -29,7 +29,7 @@ After unzip, two directories will be generated in java-tron: `bin` and `lib`, sh ## Startup -Use the corresponding script to start java-tron according to the OS type, use `*.bat` on Windows, Linux demo is as below: +Use the shell script to start java-tron (Linux / macOS): ``` # default java-tron-1.0.0/bin/FullNode @@ -45,12 +45,11 @@ java-tron-1.0.0/bin/FullNode -c config.conf -w JVM options can also be specified, located in `bin/java-tron.vmoptions`: ``` -# demo --XX:+UseConcMarkSweepGC +# demo (compatible with JDK 8 / JDK 17) +-Xms2g +-Xmx9g -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps --XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m --XX:+CMSScavengeBeforeRemark ``` \ No newline at end of file diff --git a/docs/modular-deployment-zh.md b/docs/modular-deployment-zh.md index 54a42df7d1f..27cc2ab3856 100644 --- a/docs/modular-deployment-zh.md +++ b/docs/modular-deployment-zh.md @@ -1,8 +1,8 @@ # 模块化后的 java-tron 部署方式 -模块化后,命令行下的程序启动方式将不再使用 `java -jar FullNode.jar` 的方式启动,而是使用脚本的方式启动,本文内容基于 develop 分支。 +模块化后,推荐使用 `bin/` 目录下生成的脚本启动 java-tron。原有的 `java -jar FullNode.jar` 方式仍完全支持,作为备选方式使用。 -*原有的启动方式依然保留,但即将废弃*。 +> **支持平台**:Linux 和 macOS。不支持 Windows。 ## 下载 @@ -29,7 +29,7 @@ unzip -o java-tron-1.0.0.zip ## 启动 -不同的 os 对应不同脚本,windows 即为 bat 文件,以 linux 系统为例启动 java-tron: +使用脚本启动 java-tron(Linux / macOS): ``` # 默认配置文件启动 java-tron-1.0.0/bin/FullNode @@ -43,12 +43,11 @@ java-tron-1.0.0/bin/FullNode -c config.conf -w java-tron 支持对 jvm 参数进行配置,配置文件为 bin 目录下的 java-tron.vmoptions 文件。 ``` -# demo --XX:+UseConcMarkSweepGC +# demo(兼容 JDK 8 / JDK 17) +-Xms2g +-Xmx9g -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps --XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=256m --XX:+CMSScavengeBeforeRemark ``` \ No newline at end of file diff --git a/docs/modular-introduction-en.md b/docs/modular-introduction-en.md index eab212e9771..654fbfcf995 100644 --- a/docs/modular-introduction-en.md +++ b/docs/modular-introduction-en.md @@ -16,7 +16,7 @@ The aim of java-tron modularization is to enable developers to easily build a de ![modular-structure](https://github.com/tronprotocol/java-tron/blob/develop/docs/images/module.png) -A modularized java-tron consists of six modules: framework, protocol, common, chainbase, consensus and actuator. The function of each module is elaborated below. +A modularized java-tron consists of nine modules: framework, protocol, common, chainbase, consensus, actuator, crypto, plugins and platform. The function of each module is elaborated below. ### framework @@ -67,4 +67,15 @@ Actuator module defines the `Actuator` interface, which includes 4 different met 4. calcFee: define the logic of calculating transaction fees Depending on their businesses, developers may set up Actuator accordingly and customize the processing of different types of transactions. - \ No newline at end of file + +### crypto + +Crypto module encapsulates cryptographic primitives used across the project, including elliptic curve key operations, hash functions and signature verification. It depends only on `common` and has no dependency on other business modules, keeping cryptographic logic isolated and auditable. + +### plugins + +Plugins module provides standalone operational tools packaged as independent executable JARs, such as `Toolkit.jar` and `ArchiveManifest.jar`. These tools support database maintenance tasks like migration, compaction and lite-node data pruning, and can be run without starting a full node. + +### platform + +Platform module provides the JNI bindings for the native database engines — LevelDB and RocksDB. It is architecture-aware: LevelDB is excluded on ARM64 (Apple Silicon and Linux aarch64) where only RocksDB is supported, while both are available on x86_64. diff --git a/docs/modular-introduction-zh.md b/docs/modular-introduction-zh.md index ba2c5d4b8f5..e1a02f6b778 100644 --- a/docs/modular-introduction-zh.md +++ b/docs/modular-introduction-zh.md @@ -14,7 +14,7 @@ java-tron 模块化的目的是为了帮助开发者方便地构建出特定应 ![modular-structure](https://github.com/tronprotocol/java-tron/blob/develop/docs/images/module.png) -模块化后的 java-tron 目前分为6个模块:framework、protocol、common、chainbase、consensus、actuator,下面分别简单介绍一下各个模块的作用。 +模块化后的 java-tron 目前分为9个模块:framework、protocol、common、chainbase、consensus、actuator、crypto、plugins、platform,下面分别简单介绍一下各个模块的作用。 ### framework @@ -65,4 +65,15 @@ actuator模块定义了 Actuator 接口,该接口有4个方法: 4. calcFee: 定义交易手续费计算逻辑 开发者可以根据自身业务实现 Actuator 接口,就能实现自定义交易类型的处理。 - \ No newline at end of file + +### crypto + +crypto 模块封装了项目中使用的密码学原语,包括椭圆曲线密钥操作、哈希函数及签名验证等。该模块仅依赖 `common`,不依赖其他业务模块,保持密码学逻辑的独立性与可审计性。 + +### plugins + +plugins 模块提供独立的运维工具,打包为可单独执行的 JAR(如 `Toolkit.jar`、`ArchiveManifest.jar`)。这些工具支持数据库迁移、压缩、轻节点数据裁剪等维护任务,无需启动完整节点即可运行。 + +### platform + +platform 模块提供原生数据库引擎 LevelDB 和 RocksDB 的 JNI 绑定,与架构强相关:ARM64(Apple Silicon 及 Linux aarch64)平台仅支持 RocksDB,LevelDB 被排除;x86_64 平台两者均可使用。 diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index e9a902002ba..1318fd96527 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -15,10 +15,12 @@ package org.tron.common.application; +import com.google.common.annotations.VisibleForTesting; import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.tron.core.config.args.Args; @@ -29,6 +31,18 @@ public abstract class HttpService extends AbstractService { protected String contextPath; + protected long maxRequestSize = 4 * 1024 * 1024; // 4MB + + @VisibleForTesting + public long getMaxRequestSize() { + return this.maxRequestSize; + } + + @VisibleForTesting + public void setMaxRequestSize(long maxRequestSize) { + this.maxRequestSize = maxRequestSize; + } + @Override public void innerStart() throws Exception { if (this.apiServer != null) { @@ -63,7 +77,9 @@ protected void initServer() { protected ServletContextHandler initContextHandler() { ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath(this.contextPath); - this.apiServer.setHandler(context); + SizeLimitHandler sizeLimitHandler = new SizeLimitHandler(this.maxRequestSize, -1); + sizeLimitHandler.setHandler(context); + this.apiServer.setHandler(sizeLimitHandler); return context; } 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 83d7fd2c63d..3c81ebbac2e 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 @@ -121,6 +121,29 @@ public class Args extends CommonParameter { @Getter private static String configFilePath = ""; + // Singleton config beans — populated at startup, read-only after init. + // New code can read directly from these beans instead of CommonParameter. + @Getter + private static NodeConfig nodeConfig; + @Getter + private static VmConfig vmConfig; + @Getter + private static BlockConfig blockConfig; + @Getter + private static CommitteeConfig committeeConfig; + @Getter + private static StorageConfig storageConfig; + @Getter + private static GenesisConfig genesisConfig; + @Getter + private static MiscConfig miscConfig; + @Getter + private static RateLimiterConfig rateLimiterConfig; + @Getter + private static MetricsConfig metricsConfig; + @Getter + private static EventConfig eventConfig; + @Getter @Setter private static LocalWitnesses localWitnesses = new LocalWitnesses(); @@ -173,873 +196,609 @@ public static void setParam(final String[] args, final String confFileName) { } /** - * Apply platform-specific constraints after all config sources are resolved. - * ARM64 does not support LevelDB (native JNI library unavailable), - * so db.engine is forced to RocksDB regardless of config or CLI settings. + * Bridge VmConfig bean values to CommonParameter fields. + * Temporary until Phase 2 moves fields into domain config objects. */ - private static void applyPlatformConstraints() { - if (Arch.isArm64() - && !Constant.ROCKSDB.equalsIgnoreCase(PARAMETER.storage.getDbEngine())) { - logger.warn("ARM64 only supports RocksDB, ignoring db.engine='{}'", - PARAMETER.storage.getDbEngine()); - PARAMETER.storage.setDbEngine(Constant.ROCKSDB); - } + private static void applyVmConfig(VmConfig vm) { + PARAMETER.supportConstant = vm.isSupportConstant(); + PARAMETER.maxEnergyLimitForConstant = vm.getMaxEnergyLimitForConstant(); + PARAMETER.lruCacheSize = vm.getLruCacheSize(); + PARAMETER.minTimeRatio = vm.getMinTimeRatio(); + PARAMETER.maxTimeRatio = vm.getMaxTimeRatio(); + PARAMETER.longRunningTime = vm.getLongRunningTime(); + PARAMETER.estimateEnergy = vm.isEstimateEnergy(); + PARAMETER.estimateEnergyMaxRetry = vm.getEstimateEnergyMaxRetry(); + PARAMETER.vmTrace = vm.isVmTrace(); + PARAMETER.saveInternalTx = vm.isSaveInternalTx(); + PARAMETER.saveFeaturedInternalTx = vm.isSaveFeaturedInternalTx(); + PARAMETER.saveCancelAllUnfreezeV2Details = vm.isSaveCancelAllUnfreezeV2Details(); } + // Old applyStorageConfig removed — merged into applyStorageConfig() + /** - * Apply parameters from config file. + * Bridge StorageConfig bean to PARAMETER.storage fields. + * Reads all storage config from one StorageConfig bean instance. + * Config param is still needed for setDefaultDbOptions/setCacheStrategies/setDbRoots + * which use raw Config for dynamic nested objects. */ - public static void applyConfigParams( - final Config config) { - - Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); - Wallet.setAddressPreFixString(Constant.ADD_PRE_FIX_STRING_MAINNET); - - PARAMETER.cryptoEngine = config.hasPath(ConfigKey.CRYPTO_ENGINE) ? config - .getString(ConfigKey.CRYPTO_ENGINE) : Constant.ECKey_ENGINE; - - if (config.hasPath(ConfigKey.VM_SUPPORT_CONSTANT)) { - PARAMETER.supportConstant = config.getBoolean(ConfigKey.VM_SUPPORT_CONSTANT); - } - - if (config.hasPath(ConfigKey.VM_MAX_ENERGY_LIMIT_FOR_CONSTANT)) { - long configLimit = config.getLong(ConfigKey.VM_MAX_ENERGY_LIMIT_FOR_CONSTANT); - PARAMETER.maxEnergyLimitForConstant = max(ENERGY_LIMIT_IN_CONSTANT_TX, configLimit, true); - } - - if (config.hasPath(ConfigKey.VM_LRU_CACHE_SIZE)) { - PARAMETER.lruCacheSize = config.getInt(ConfigKey.VM_LRU_CACHE_SIZE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_ENABLE)) { - PARAMETER.rpcEnable = config.getBoolean(ConfigKey.NODE_RPC_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_SOLIDITY_ENABLE)) { - PARAMETER.rpcSolidityEnable = config.getBoolean(ConfigKey.NODE_RPC_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_RPC_PBFT_ENABLE)) { - PARAMETER.rpcPBFTEnable = config.getBoolean(ConfigKey.NODE_RPC_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_FULLNODE_ENABLE)) { - PARAMETER.fullNodeHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_FULLNODE_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_SOLIDITY_ENABLE)) { - PARAMETER.solidityNodeHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_HTTP_PBFT_ENABLE)) { - PARAMETER.pBFTHttpEnable = config.getBoolean(ConfigKey.NODE_HTTP_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_ENABLE)) { - PARAMETER.jsonRpcHttpFullNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_ENABLE)) { - PARAMETER.jsonRpcHttpSolidityNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_PBFT_ENABLE)) { - PARAMETER.jsonRpcHttpPBFTNodeEnable = - config.getBoolean(ConfigKey.NODE_JSONRPC_HTTP_PBFT_ENABLE); - } - - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_BLOCK_RANGE)) { - PARAMETER.jsonRpcMaxBlockRange = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_BLOCK_RANGE); - } + 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 + PARAMETER.storage.setCheckpointVersion(sc.getCheckpoint().getVersion()); + PARAMETER.storage.setCheckpointSync(sc.getCheckpoint().isSync()); + + // estimatedTransactions / maxFlushCount clamping & validation run inside + // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. + PARAMETER.storage.setEstimatedBlockTransactions(sc.getTxCache().getEstimatedTransactions()); + PARAMETER.storage.setTxCacheInitOptimization(sc.getTxCache().isInitOptimization()); + PARAMETER.storage.setMaxFlushCount(sc.getSnapshot().getMaxFlushCount()); + + // backup + StorageConfig.BackupConfig backup = sc.getBackup(); + PARAMETER.dbBackupConfig = DbBackupConfig.getInstance() + .initArgs(backup.isEnable(), backup.getPropPath(), + backup.getBak1path(), backup.getBak2path(), backup.getFrequency()); - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_SUB_TOPICS)) { - PARAMETER.jsonRpcMaxSubTopics = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_SUB_TOPICS); - } + // RocksDB settings + StorageConfig.DbSettingsConfig dbs = sc.getDbSettings(); + PARAMETER.rocksDBCustomSettings = RocksDbSettings + .initCustomSettings(dbs.getLevelNumber(), dbs.getCompactThreads(), + dbs.getBlocksize(), dbs.getMaxBytesForLevelBase(), + dbs.getMaxBytesForLevelMultiplier(), dbs.getLevel0FileNumCompactionTrigger(), + dbs.getTargetFileSizeBase(), dbs.getTargetFileSizeMultiplier(), + dbs.getMaxOpenFiles()); + RocksDbSettings.loggingSettings(); - if (config.hasPath(ConfigKey.NODE_JSONRPC_MAX_BLOCK_FILTER_NUM)) { - PARAMETER.jsonRpcMaxBlockFilterNum = - config.getInt(ConfigKey.NODE_JSONRPC_MAX_BLOCK_FILTER_NUM); - } + // Dynamic nested objects use StorageConfig's raw storage sub-tree + // setDefaultDbOptions must be called before setPropertyMapFromBean because + // createPropertyFromBean calls newDefaultDbOptions which needs defaultDbOptions initialized + PARAMETER.storage.setDefaultDbOptions(sc); + PARAMETER.storage.setPropertyMapFromBean(sc.getProperties()); + PARAMETER.storage.setCacheStrategies(sc.getRawStorageConfig()); + PARAMETER.storage.setDbRoots(sc.getRawStorageConfig()); + } - if (config.hasPath(ConfigKey.VM_MIN_TIME_RATIO)) { - PARAMETER.minTimeRatio = config.getDouble(ConfigKey.VM_MIN_TIME_RATIO); - } + /** + * Bridge NodeConfig backup sub-bean to PARAMETER fields. + */ + private static void applyNodeBackupConfig(NodeConfig nc) { + NodeConfig.NodeBackupConfig b = nc.getBackup(); + PARAMETER.backupPriority = b.getPriority(); + PARAMETER.backupPort = b.getPort(); + PARAMETER.keepAliveInterval = b.getKeepAliveInterval(); + PARAMETER.backupMembers = b.getMembers(); + } - if (config.hasPath(ConfigKey.VM_MAX_TIME_RATIO)) { - PARAMETER.maxTimeRatio = config.getDouble(ConfigKey.VM_MAX_TIME_RATIO); + /** + * Bridge GenesisConfig bean to GenesisBlock business object. + * Converts raw address strings via Base58Check decoding. + */ + private static void applyGenesisConfig(GenesisConfig gc, Config config) { + if (gc.getTimestamp().isEmpty() && gc.getAssets().isEmpty()) { + PARAMETER.genesisBlock = GenesisBlock.getDefault(); + return; } - - if (config.hasPath(ConfigKey.VM_LONG_RUNNING_TIME)) { - PARAMETER.longRunningTime = config.getInt(ConfigKey.VM_LONG_RUNNING_TIME); + PARAMETER.genesisBlock = new GenesisBlock(); + PARAMETER.genesisBlock.setTimestamp(gc.getTimestamp()); + PARAMETER.genesisBlock.setParentHash(gc.getParentHash()); + + if (!gc.getAssets().isEmpty()) { + List accounts = new ArrayList<>(); + for (GenesisConfig.AssetConfig ac : gc.getAssets()) { + Account account = new Account(); + account.setAccountName(ac.getAccountName()); + account.setAccountType(ac.getAccountType()); + account.setAddress(Commons.decodeFromBase58Check(ac.getAddress())); + account.setBalance(ac.getBalance()); + accounts.add(account); + } + PARAMETER.genesisBlock.setAssets(accounts); + AccountStore.setAccount(config); + } + { + List witnesses = new ArrayList<>(); + for (GenesisConfig.WitnessConfig wc : gc.getWitnesses()) { + Witness witness = new Witness(); + witness.setAddress(Commons.decodeFromBase58Check(wc.getAddress())); + witness.setUrl(wc.getUrl()); + witness.setVoteCount(wc.getVoteCount()); + witnesses.add(witness); + } + PARAMETER.genesisBlock.setWitnesses(witnesses); } + } - PARAMETER.storage = new Storage(); - - PARAMETER.storage.setDbEngine(Storage.getDbEngineFromConfig(config)); - PARAMETER.storage.setDbSync(Storage.getDbVersionSyncFromConfig(config)); - PARAMETER.storage.setContractParseSwitch(Storage.getContractParseSwitchFromConfig(config)); - PARAMETER.storage.setDbDirectory(Storage.getDbDirectoryFromConfig(config)); - PARAMETER.storage.setIndexDirectory(Storage.getIndexDirectoryFromConfig(config)); - PARAMETER.storage.setIndexSwitch(Storage.getIndexSwitchFromConfig(config)); - PARAMETER.storage.setTransactionHistorySwitch( - Storage.getTransactionHistorySwitchFromConfig(config)); - - PARAMETER.storage - .setCheckpointVersion(Storage.getCheckpointVersionFromConfig(config)); - PARAMETER.storage - .setCheckpointSync(Storage.getCheckpointSyncFromConfig(config)); - - PARAMETER.storage.setEstimatedBlockTransactions( - Storage.getEstimatedTransactionsFromConfig(config)); - PARAMETER.storage.setTxCacheInitOptimization( - Storage.getTxCacheInitOptimizationFromConfig(config)); - PARAMETER.storage.setMaxFlushCount(Storage.getSnapshotMaxFlushCountFromConfig(config)); - - PARAMETER.storage.setDefaultDbOptions(config); - PARAMETER.storage.setPropertyMapFromConfig(config); - PARAMETER.storage.setCacheStrategies(config); - PARAMETER.storage.setDbRoots(config); - + /** + * Bridge MiscConfig bean values to CommonParameter fields. + */ + private static void applyMiscConfig(MiscConfig mc) { + PARAMETER.cryptoEngine = mc.getCryptoEngine(); + PARAMETER.needToUpdateAsset = mc.isNeedToUpdateAsset(); + PARAMETER.historyBalanceLookup = mc.isHistoryBalanceLookup(); + PARAMETER.trxReferenceBlock = mc.getTrxReferenceBlock(); + PARAMETER.trxExpirationTimeInMilliseconds = mc.getTrxExpirationTimeInMilliseconds(); + PARAMETER.blockNumForEnergyLimit = mc.getBlockNumForEnergyLimit(); + PARAMETER.actuatorSet = mc.getActuatorWhitelist(); + + // seed.node — top-level config section, not under "node" + // Config structure is arguably misplaced but preserved for backward compatibility PARAMETER.seedNode = new SeedNode(); PARAMETER.seedNode.setAddressList( - getInetSocketAddress(config, ConfigKey.SEED_NODE_IP_LIST, false)); - - if (config.hasPath(ConfigKey.GENESIS_BLOCK)) { - PARAMETER.genesisBlock = new GenesisBlock(); + mc.getSeedNodeIpList().stream() + .map(s -> org.tron.p2p.utils.NetUtil.parseInetSocketAddress(s)) + .collect(Collectors.toList())); + } - PARAMETER.genesisBlock.setTimestamp(config.getString(ConfigKey.GENESIS_BLOCK_TIMESTAMP)); - PARAMETER.genesisBlock.setParentHash(config.getString(ConfigKey.GENESIS_BLOCK_PARENTHASH)); + /** + * Bridge RateLimiterConfig bean values to CommonParameter fields. + * HTTP/RPC rate limiter lists still use getRateLimiterFromConfig() for + * conversion to RateLimiterInitialization business objects. + */ + private static void applyRateLimiterConfig(RateLimiterConfig rl) { + PARAMETER.rateLimiterGlobalQps = rl.getGlobal().getQps(); + PARAMETER.rateLimiterGlobalIpQps = rl.getGlobal().getIp().getQps(); + PARAMETER.rateLimiterGlobalApiQps = rl.getGlobal().getApi().getQps(); + PARAMETER.rateLimiterSyncBlockChain = rl.getP2p().getSyncBlockChain(); + PARAMETER.rateLimiterFetchInvData = rl.getP2p().getFetchInvData(); + PARAMETER.rateLimiterDisconnect = rl.getP2p().getDisconnect(); + + // HTTP/RPC rate limiter items: convert bean lists to business objects + RateLimiterInitialization initialization = new RateLimiterInitialization(); + ArrayList httpItems = new ArrayList<>(); + for (RateLimiterConfig.HttpRateLimitItem item : rl.getHttp()) { + httpItems.add(new RateLimiterInitialization.HttpRateLimiterItem( + item.getComponent(), item.getStrategy(), item.getParamString())); + } + initialization.setHttpMap(httpItems); + ArrayList rpcItems = new ArrayList<>(); + for (RateLimiterConfig.RpcRateLimitItem item : rl.getRpc()) { + rpcItems.add(new RateLimiterInitialization.RpcRateLimiterItem( + item.getComponent(), item.getStrategy(), item.getParamString())); + } + initialization.setRpcMap(rpcItems); + PARAMETER.rateLimiterInitialization = initialization; + } - if (config.hasPath(ConfigKey.GENESIS_BLOCK_ASSETS)) { - PARAMETER.genesisBlock.setAssets(getAccountsFromConfig(config)); - AccountStore.setAccount(config); - } - if (config.hasPath(ConfigKey.GENESIS_BLOCK_WITNESSES)) { - PARAMETER.genesisBlock.setWitnesses(getWitnessesFromConfig(config)); + /** + * 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()); + + // Build EventPluginConfig from EventConfig bean + // If event.subscribe was configured, bean will have non-default values + if (ec.isEnable() || ec.getVersion() != 0 || !ec.getTopics().isEmpty() + || StringUtils.isNotEmpty(ec.getPath()) || StringUtils.isNotEmpty(ec.getServer())) { + EventPluginConfig epc = new EventPluginConfig(); + epc.setVersion(ec.getVersion()); + epc.setStartSyncBlockNum(ec.getStartSyncBlockNum()); + + // native queue + EventConfig.NativeConfig nq = ec.getNativeQueue(); + epc.setUseNativeQueue(nq.isUseNativeQueue()); + epc.setBindPort(nq.getBindport()); + epc.setSendQueueLength(nq.getSendqueuelength()); + + if (!nq.isUseNativeQueue()) { + if (StringUtils.isNotEmpty(ec.getPath())) { + epc.setPluginPath(ec.getPath().trim()); + } + if (StringUtils.isNotEmpty(ec.getServer())) { + epc.setServerAddress(ec.getServer().trim()); + } + if (StringUtils.isNotEmpty(ec.getDbconfig())) { + epc.setDbConfig(ec.getDbconfig().trim()); + } } - } else { - PARAMETER.genesisBlock = GenesisBlock.getDefault(); - } - - PARAMETER.needSyncCheck = - config.hasPath(ConfigKey.BLOCK_NEED_SYNC_CHECK) - && config.getBoolean(ConfigKey.BLOCK_NEED_SYNC_CHECK); - - PARAMETER.nodeDiscoveryEnable = - config.hasPath(ConfigKey.NODE_DISCOVERY_ENABLE) - && config.getBoolean(ConfigKey.NODE_DISCOVERY_ENABLE); - - PARAMETER.nodeDiscoveryPersist = - config.hasPath(ConfigKey.NODE_DISCOVERY_PERSIST) - && config.getBoolean(ConfigKey.NODE_DISCOVERY_PERSIST); - - PARAMETER.nodeEffectiveCheckEnable = - config.hasPath(ConfigKey.NODE_EFFECTIVE_CHECK_ENABLE) - && config.getBoolean(ConfigKey.NODE_EFFECTIVE_CHECK_ENABLE); - - PARAMETER.nodeConnectionTimeout = - config.hasPath(ConfigKey.NODE_CONNECTION_TIMEOUT) - ? config.getInt(ConfigKey.NODE_CONNECTION_TIMEOUT) * 1000 - : 2000; - - if (!config.hasPath(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT)) { - PARAMETER.fetchBlockTimeout = 500; - } else if (config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT) > 1000) { - PARAMETER.fetchBlockTimeout = 1000; - } else if (config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT) < 100) { - PARAMETER.fetchBlockTimeout = 100; - } else { - PARAMETER.fetchBlockTimeout = config.getInt(ConfigKey.NODE_FETCH_BLOCK_TIMEOUT); - } - - PARAMETER.nodeChannelReadTimeout = - config.hasPath(ConfigKey.NODE_CHANNEL_READ_TIMEOUT) - ? config.getInt(ConfigKey.NODE_CHANNEL_READ_TIMEOUT) - : 0; - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES)) { - PARAMETER.maxConnections = config.getInt(ConfigKey.NODE_MAX_ACTIVE_NODES); - } else { - PARAMETER.maxConnections = - config.hasPath(ConfigKey.NODE_MAX_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MAX_CONNECTIONS) : 30; - } - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES) - && config.hasPath(ConfigKey.NODE_CONNECT_FACTOR)) { - PARAMETER.minConnections = (int) (PARAMETER.maxConnections - * config.getDouble(ConfigKey.NODE_CONNECT_FACTOR)); - } else { - PARAMETER.minConnections = - config.hasPath(ConfigKey.NODE_MIN_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MIN_CONNECTIONS) : 8; - } - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES) - && config.hasPath(ConfigKey.NODE_ACTIVE_CONNECT_FACTOR)) { - PARAMETER.minActiveConnections = (int) (PARAMETER.maxConnections - * config.getDouble(ConfigKey.NODE_ACTIVE_CONNECT_FACTOR)); - } else { - PARAMETER.minActiveConnections = - config.hasPath(ConfigKey.NODE_MIN_ACTIVE_CONNECTIONS) - ? config.getInt(ConfigKey.NODE_MIN_ACTIVE_CONNECTIONS) : 3; - } - - if (config.hasPath(ConfigKey.NODE_MAX_ACTIVE_NODES_WITH_SAME_IP)) { - PARAMETER.maxConnectionsWithSameIp = - config.getInt(ConfigKey.NODE_MAX_ACTIVE_NODES_WITH_SAME_IP); - } else { - PARAMETER.maxConnectionsWithSameIp = - config.hasPath(ConfigKey.NODE_MAX_CONNECTIONS_WITH_SAME_IP) ? config - .getInt(ConfigKey.NODE_MAX_CONNECTIONS_WITH_SAME_IP) : 2; - } - - PARAMETER.maxTps = config.hasPath(ConfigKey.NODE_MAX_TPS) - ? config.getInt(ConfigKey.NODE_MAX_TPS) : 1000; - - PARAMETER.minParticipationRate = - config.hasPath(ConfigKey.NODE_MIN_PARTICIPATION_RATE) - ? config.getInt(ConfigKey.NODE_MIN_PARTICIPATION_RATE) - : 0; - - PARAMETER.p2pConfig = new P2pConfig(); - PARAMETER.nodeListenPort = - config.hasPath(ConfigKey.NODE_LISTEN_PORT) - ? config.getInt(ConfigKey.NODE_LISTEN_PORT) : 0; - - PARAMETER.nodeLanIp = PARAMETER.p2pConfig.getLanIp(); - externalIp(config); - - PARAMETER.nodeP2pVersion = - config.hasPath(ConfigKey.NODE_P2P_VERSION) - ? config.getInt(ConfigKey.NODE_P2P_VERSION) : 0; - - PARAMETER.nodeEnableIpv6 = - config.hasPath(ConfigKey.NODE_ENABLE_IPV6) && config.getBoolean(ConfigKey.NODE_ENABLE_IPV6); - - PARAMETER.dnsTreeUrls = config.hasPath(ConfigKey.NODE_DNS_TREE_URLS) ? config.getStringList( - ConfigKey.NODE_DNS_TREE_URLS) : new ArrayList<>(); - - PARAMETER.dnsPublishConfig = loadDnsPublishConfig(config); + // topics + List triggerConfigs = new ArrayList<>(); + for (EventConfig.TopicConfig tc : ec.getTopics()) { + TriggerConfig trig = new TriggerConfig(); + trig.setTriggerName(tc.getTriggerName()); + trig.setEnabled(tc.isEnable()); + trig.setTopic(tc.getTopic()); + trig.setSolidified(tc.isSolidified()); + trig.setEthCompatible(tc.isEthCompatible()); + trig.setRedundancy(tc.isRedundancy()); + triggerConfigs.add(trig); + } + epc.setTriggerConfigList(triggerConfigs); - PARAMETER.syncFetchBatchNum = config.hasPath(ConfigKey.NODE_SYNC_FETCH_BATCH_NUM) ? config - .getInt(ConfigKey.NODE_SYNC_FETCH_BATCH_NUM) : 2000; - if (PARAMETER.syncFetchBatchNum > 2000) { - PARAMETER.syncFetchBatchNum = 2000; - } - if (PARAMETER.syncFetchBatchNum < 100) { - PARAMETER.syncFetchBatchNum = 100; + PARAMETER.eventPluginConfig = epc; } - PARAMETER.rpcPort = - config.hasPath(ConfigKey.NODE_RPC_PORT) - ? config.getInt(ConfigKey.NODE_RPC_PORT) : 50051; - - PARAMETER.rpcOnSolidityPort = - config.hasPath(ConfigKey.NODE_RPC_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_RPC_SOLIDITY_PORT) : 50061; - - PARAMETER.rpcOnPBFTPort = - config.hasPath(ConfigKey.NODE_RPC_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_RPC_PBFT_PORT) : 50071; - - PARAMETER.fullNodeHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_FULLNODE_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_FULLNODE_PORT) : 8090; - - PARAMETER.solidityHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_SOLIDITY_PORT) : 8091; - - PARAMETER.pBFTHttpPort = - config.hasPath(ConfigKey.NODE_HTTP_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_HTTP_PBFT_PORT) : 8092; - - PARAMETER.jsonRpcHttpFullNodePort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_FULLNODE_PORT) : 8545; - - PARAMETER.jsonRpcHttpSolidityPort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_SOLIDITY_PORT) : 8555; - - PARAMETER.jsonRpcHttpPBFTPort = - config.hasPath(ConfigKey.NODE_JSONRPC_HTTP_PBFT_PORT) - ? config.getInt(ConfigKey.NODE_JSONRPC_HTTP_PBFT_PORT) : 8565; - - PARAMETER.rpcThreadNum = - config.hasPath(ConfigKey.NODE_RPC_THREAD) ? config.getInt(ConfigKey.NODE_RPC_THREAD) - : (Runtime.getRuntime().availableProcessors() + 1) / 2; - - PARAMETER.solidityThreads = - config.hasPath(ConfigKey.NODE_SOLIDITY_THREADS) - ? config.getInt(ConfigKey.NODE_SOLIDITY_THREADS) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.maxConcurrentCallsPerConnection = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION) - ? config.getInt(ConfigKey.NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION) - : Integer.MAX_VALUE; - - PARAMETER.flowControlWindow = config.hasPath(ConfigKey.NODE_RPC_FLOW_CONTROL_WINDOW) - ? config.getInt(ConfigKey.NODE_RPC_FLOW_CONTROL_WINDOW) - : NettyServerBuilder.DEFAULT_FLOW_CONTROL_WINDOW; - if (config.hasPath(ConfigKey.NODE_RPC_MAX_RST_STREAM)) { - PARAMETER.rpcMaxRstStream = config.getInt(ConfigKey.NODE_RPC_MAX_RST_STREAM); - } - if (config.hasPath(ConfigKey.NODE_RPC_SECONDS_PER_WINDOW)) { - PARAMETER.rpcSecondsPerWindow = config.getInt(ConfigKey.NODE_RPC_SECONDS_PER_WINDOW); - } + // Build FilterQuery from EventConfig.FilterConfig bean + EventConfig.FilterConfig fc = ec.getFilter(); + if (StringUtils.isNotEmpty(fc.getFromblock()) || StringUtils.isNotEmpty(fc.getToblock()) + || !fc.getContractAddress().isEmpty()) { + FilterQuery filter = new FilterQuery(); - PARAMETER.maxConnectionIdleInMillis = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS) - ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS) - : Long.MAX_VALUE; + try { + filter.setFromBlock(FilterQuery.parseFromBlockNumber(fc.getFromblock().trim())); + } catch (Exception e) { + logger.error("invalid filter: fromBlockNumber: {}", fc.getFromblock(), e); + PARAMETER.eventFilter = null; + return; + } - PARAMETER.blockProducedTimeOut = config.hasPath(ConfigKey.NODE_PRODUCED_TIMEOUT) - ? config.getInt(ConfigKey.NODE_PRODUCED_TIMEOUT) : BLOCK_PRODUCE_TIMEOUT_PERCENT; + try { + filter.setToBlock(FilterQuery.parseToBlockNumber(fc.getToblock().trim())); + } catch (Exception e) { + logger.error("invalid filter: toBlockNumber: {}", fc.getToblock(), e); + PARAMETER.eventFilter = null; + return; + } - PARAMETER.maxHttpConnectNumber = config.hasPath(ConfigKey.NODE_MAX_HTTP_CONNECT_NUMBER) - ? config.getInt(ConfigKey.NODE_MAX_HTTP_CONNECT_NUMBER) - : NodeConstant.MAX_HTTP_CONNECT_NUMBER; + filter.setContractAddressList( + fc.getContractAddress().stream() + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList())); + filter.setContractTopicList( + fc.getContractTopic().stream() + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList())); - if (PARAMETER.blockProducedTimeOut < 30) { - PARAMETER.blockProducedTimeOut = 30; - } - if (PARAMETER.blockProducedTimeOut > 100) { - PARAMETER.blockProducedTimeOut = 100; + PARAMETER.eventFilter = filter; } + } - PARAMETER.netMaxTrxPerSecond = config.hasPath(ConfigKey.NODE_NET_MAX_TRX_PER_SECOND) - ? config.getInt(ConfigKey.NODE_NET_MAX_TRX_PER_SECOND) - : NetConstants.NET_MAX_TRX_PER_SECOND; - - PARAMETER.maxConnectionAgeInMillis = - config.hasPath(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) - ? config.getLong(ConfigKey.NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS) - : Long.MAX_VALUE; - - PARAMETER.maxMessageSize = config.hasPath(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_MESSAGE_SIZE) : GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; - - PARAMETER.maxHeaderListSize = config.hasPath(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) - ? config.getInt(ConfigKey.NODE_RPC_MAX_HEADER_LIST_SIZE) - : GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; - - PARAMETER.isRpcReflectionServiceEnable = - config.hasPath(ConfigKey.NODE_RPC_REFLECTION_SERVICE) - && config.getBoolean(ConfigKey.NODE_RPC_REFLECTION_SERVICE); - - PARAMETER.maintenanceTimeInterval = - config.hasPath(ConfigKey.BLOCK_MAINTENANCE_TIME_INTERVAL) ? config - .getInt(ConfigKey.BLOCK_MAINTENANCE_TIME_INTERVAL) : 21600000L; - - PARAMETER.proposalExpireTime = getProposalExpirationTime(config); - - PARAMETER.checkFrozenTime = - config.hasPath(ConfigKey.BLOCK_CHECK_FROZEN_TIME) ? config - .getInt(ConfigKey.BLOCK_CHECK_FROZEN_TIME) : 1; - - PARAMETER.allowCreationOfContracts = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_CREATION_OF_CONTRACTS) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_CREATION_OF_CONTRACTS) : 0; - - PARAMETER.allowMultiSign = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_MULTI_SIGN) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_MULTI_SIGN) : 0; - - PARAMETER.allowAdaptiveEnergy = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ADAPTIVE_ENERGY) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ADAPTIVE_ENERGY) : 0; - - PARAMETER.allowDelegateResource = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_DELEGATE_RESOURCE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_DELEGATE_RESOURCE) : 0; - - PARAMETER.allowSameTokenName = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_SAME_TOKEN_NAME) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_SAME_TOKEN_NAME) : 0; - - PARAMETER.allowTvmTransferTrc10 = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_TRANSFER_TRC10) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_TRANSFER_TRC10) : 0; - - PARAMETER.allowTvmConstantinople = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_CONSTANTINOPLE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_CONSTANTINOPLE) : 0; - - PARAMETER.allowTvmSolidity059 = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_SOLIDITY059) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_SOLIDITY059) : 0; + /** + * Bridge MetricsConfig bean values to CommonParameter fields. + * Note: node.metricsEnable is handled in applyNodeConfig (it's a node-level field). + */ + private static void applyMetricsConfig(MetricsConfig mc) { + PARAMETER.metricsStorageEnable = mc.isStorageEnable(); + PARAMETER.influxDbIp = mc.getInfluxdb().getIp().isEmpty() + ? Constant.LOCAL_HOST : mc.getInfluxdb().getIp(); + PARAMETER.influxDbPort = mc.getInfluxdb().getPort(); + PARAMETER.influxDbDatabase = mc.getInfluxdb().getDatabase(); + PARAMETER.metricsReportInterval = mc.getInfluxdb().getMetricsReportInterval(); + PARAMETER.metricsPrometheusEnable = mc.getPrometheus().isEnable(); + PARAMETER.metricsPrometheusPort = mc.getPrometheus().getPort(); + } - PARAMETER.forbidTransferToContract = - config.hasPath(ConfigKey.COMMITTEE_FORBID_TRANSFER_TO_CONTRACT) ? config - .getInt(ConfigKey.COMMITTEE_FORBID_TRANSFER_TO_CONTRACT) : 0; + /** + * Bridge CommitteeConfig bean values to CommonParameter fields. + */ + private static void applyCommitteeConfig(CommitteeConfig cc) { + PARAMETER.allowCreationOfContracts = cc.getAllowCreationOfContracts(); + PARAMETER.allowMultiSign = (int) cc.getAllowMultiSign(); + PARAMETER.allowAdaptiveEnergy = cc.getAllowAdaptiveEnergy(); + PARAMETER.allowDelegateResource = cc.getAllowDelegateResource(); + PARAMETER.allowSameTokenName = cc.getAllowSameTokenName(); + PARAMETER.allowTvmTransferTrc10 = cc.getAllowTvmTransferTrc10(); + PARAMETER.allowTvmConstantinople = cc.getAllowTvmConstantinople(); + PARAMETER.allowTvmSolidity059 = cc.getAllowTvmSolidity059(); + PARAMETER.forbidTransferToContract = cc.getForbidTransferToContract(); + PARAMETER.allowShieldedTRC20Transaction = cc.getAllowShieldedTRC20Transaction(); + PARAMETER.allowMarketTransaction = cc.getAllowMarketTransaction(); + PARAMETER.allowTransactionFeePool = cc.getAllowTransactionFeePool(); + PARAMETER.allowBlackHoleOptimization = cc.getAllowBlackHoleOptimization(); + PARAMETER.allowNewResourceModel = cc.getAllowNewResourceModel(); + PARAMETER.allowTvmIstanbul = cc.getAllowTvmIstanbul(); + PARAMETER.allowProtoFilterNum = cc.getAllowProtoFilterNum(); + PARAMETER.allowAccountStateRoot = cc.getAllowAccountStateRoot(); + PARAMETER.changedDelegation = cc.getChangedDelegation(); + PARAMETER.allowPBFT = cc.getAllowPBFT(); + PARAMETER.pBFTExpireNum = cc.getPBFTExpireNum(); + PARAMETER.allowTvmFreeze = cc.getAllowTvmFreeze(); + PARAMETER.allowTvmVote = cc.getAllowTvmVote(); + PARAMETER.allowTvmLondon = cc.getAllowTvmLondon(); + PARAMETER.allowTvmCompatibleEvm = cc.getAllowTvmCompatibleEvm(); + PARAMETER.allowHigherLimitForMaxCpuTimeOfOneTx = + cc.getAllowHigherLimitForMaxCpuTimeOfOneTx(); + PARAMETER.allowNewRewardAlgorithm = cc.getAllowNewRewardAlgorithm(); + PARAMETER.allowOptimizedReturnValueOfChainId = + cc.getAllowOptimizedReturnValueOfChainId(); + PARAMETER.allowTvmShangHai = cc.getAllowTvmShangHai(); + PARAMETER.allowOldRewardOpt = cc.getAllowOldRewardOpt(); + PARAMETER.allowEnergyAdjustment = cc.getAllowEnergyAdjustment(); + PARAMETER.allowStrictMath = cc.getAllowStrictMath(); + PARAMETER.consensusLogicOptimization = cc.getConsensusLogicOptimization(); + PARAMETER.allowTvmCancun = cc.getAllowTvmCancun(); + PARAMETER.allowTvmBlob = cc.getAllowTvmBlob(); + PARAMETER.allowTvmOsaka = cc.getAllowTvmOsaka(); + PARAMETER.unfreezeDelayDays = cc.getUnfreezeDelayDays(); + // allowReceiptsMerkleRoot not in CommonParameter — skip for now + PARAMETER.allowAccountAssetOptimization = cc.getAllowAccountAssetOptimization(); + PARAMETER.allowAssetOptimization = cc.getAllowAssetOptimization(); + PARAMETER.allowNewReward = cc.getAllowNewReward(); + PARAMETER.memoFee = cc.getMemoFee(); + PARAMETER.allowDelegateOptimization = cc.getAllowDelegateOptimization(); + PARAMETER.allowDynamicEnergy = cc.getAllowDynamicEnergy(); + PARAMETER.dynamicEnergyThreshold = cc.getDynamicEnergyThreshold(); + PARAMETER.dynamicEnergyIncreaseFactor = cc.getDynamicEnergyIncreaseFactor(); + PARAMETER.dynamicEnergyMaxFactor = cc.getDynamicEnergyMaxFactor(); + } - PARAMETER.tcpNettyWorkThreadNum = config.hasPath(ConfigKey.NODE_TCP_NETTY_WORK_THREAD_NUM) - ? config.getInt(ConfigKey.NODE_TCP_NETTY_WORK_THREAD_NUM) : 0; + /** + * Bridge BlockConfig bean values to CommonParameter fields. + */ + private static void applyBlockConfig(BlockConfig block) { + PARAMETER.needSyncCheck = block.isNeedSyncCheck(); + PARAMETER.maintenanceTimeInterval = block.getMaintenanceTimeInterval(); + PARAMETER.proposalExpireTime = block.getProposalExpireTime(); + PARAMETER.checkFrozenTime = block.getCheckFrozenTime(); + } - PARAMETER.udpNettyWorkThreadNum = config.hasPath(ConfigKey.NODE_UDP_NETTY_WORK_THREAD_NUM) - ? config.getInt(ConfigKey.NODE_UDP_NETTY_WORK_THREAD_NUM) : 1; + /** + * Bridge NodeConfig bean values to CommonParameter fields. + * Some fields require post-binding range checks or dynamic defaults (e.g. CPUs/2), + * which are applied here after copying the bean value. + * + * @param nc the NodeConfig bean populated from config.conf "node" section + * node.discovery / node.channel.read.timeout (dot-notation paths + * not part of the NodeConfig bean) + */ + @SuppressWarnings("checkstyle:MethodLength") + private static void applyNodeConfig(NodeConfig nc) { + // ---- RPC sub-bean ---- + NodeConfig.RpcConfig rpc = nc.getRpc(); + PARAMETER.rpcEnable = rpc.isEnable(); + PARAMETER.rpcSolidityEnable = rpc.isSolidityEnable(); + PARAMETER.rpcPBFTEnable = rpc.isPBFTEnable(); + PARAMETER.rpcPort = rpc.getPort(); + PARAMETER.rpcOnSolidityPort = rpc.getSolidityPort(); + PARAMETER.rpcOnPBFTPort = rpc.getPBFTPort(); + PARAMETER.rpcThreadNum = rpc.getThread(); + PARAMETER.maxConcurrentCallsPerConnection = rpc.getMaxConcurrentCallsPerConnection(); + PARAMETER.flowControlWindow = rpc.getFlowControlWindow(); + PARAMETER.rpcMaxRstStream = rpc.getMaxRstStream(); + PARAMETER.rpcSecondsPerWindow = rpc.getSecondsPerWindow(); + PARAMETER.maxConnectionIdleInMillis = rpc.getMaxConnectionIdleInMillis(); + PARAMETER.maxConnectionAgeInMillis = rpc.getMaxConnectionAgeInMillis(); + PARAMETER.maxMessageSize = rpc.getMaxMessageSize(); + PARAMETER.maxHeaderListSize = rpc.getMaxHeaderListSize(); + PARAMETER.isRpcReflectionServiceEnable = rpc.isReflectionService(); + PARAMETER.minEffectiveConnection = rpc.getMinEffectiveConnection(); + PARAMETER.trxCacheEnable = rpc.isTrxCacheEnable(); + + // ---- HTTP sub-bean ---- + NodeConfig.HttpConfig http = nc.getHttp(); + PARAMETER.fullNodeHttpEnable = http.isFullNodeEnable(); + PARAMETER.solidityNodeHttpEnable = http.isSolidityEnable(); + PARAMETER.pBFTHttpEnable = http.isPBFTEnable(); + PARAMETER.fullNodeHttpPort = http.getFullNodePort(); + PARAMETER.solidityHttpPort = http.getSolidityPort(); + PARAMETER.pBFTHttpPort = http.getPBFTPort(); + PARAMETER.httpMaxMessageSize = http.getMaxMessageSize(); + + // ---- JSON-RPC sub-bean ---- + NodeConfig.JsonRpcConfig jsonrpc = nc.getJsonrpc(); + PARAMETER.jsonRpcHttpFullNodeEnable = jsonrpc.isHttpFullNodeEnable(); + PARAMETER.jsonRpcHttpSolidityNodeEnable = jsonrpc.isHttpSolidityEnable(); + PARAMETER.jsonRpcHttpPBFTNodeEnable = jsonrpc.isHttpPBFTEnable(); + PARAMETER.jsonRpcHttpFullNodePort = jsonrpc.getHttpFullNodePort(); + PARAMETER.jsonRpcHttpSolidityPort = jsonrpc.getHttpSolidityPort(); + PARAMETER.jsonRpcHttpPBFTPort = jsonrpc.getHttpPBFTPort(); + PARAMETER.jsonRpcMaxBlockRange = jsonrpc.getMaxBlockRange(); + PARAMETER.jsonRpcMaxSubTopics = jsonrpc.getMaxSubTopics(); + PARAMETER.jsonRpcMaxBlockFilterNum = jsonrpc.getMaxBlockFilterNum(); + PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize(); + + // ---- P2P sub-bean ---- + PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); + + // ---- DNS sub-bean (tree URLs only — publish config uses complex validation) ---- + PARAMETER.dnsTreeUrls = nc.getDns().getTreeUrls().isEmpty() + ? new ArrayList<>() : new ArrayList<>(nc.getDns().getTreeUrls()); + + // ---- Dynamic config sub-bean ---- + PARAMETER.dynamicConfigEnable = nc.getDynamicConfig().isEnable(); + PARAMETER.dynamicConfigCheckInterval = nc.getDynamicConfig().getCheckInterval(); + + // ---- Flat scalar fields ---- + PARAMETER.nodeEffectiveCheckEnable = nc.isEffectiveCheckEnable(); + PARAMETER.nodeConnectionTimeout = nc.getConnectionTimeout() * 1000; + + // 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.maxConnections = nc.getMaxConnections(); + PARAMETER.minConnections = nc.getMinConnections(); + PARAMETER.minActiveConnections = nc.getMinActiveConnections(); + PARAMETER.maxConnectionsWithSameIp = nc.getMaxConnectionsWithSameIp(); + PARAMETER.maxTps = nc.getMaxTps(); + PARAMETER.minParticipationRate = nc.getMinParticipationRate(); + PARAMETER.nodeListenPort = nc.getListenPort(); + PARAMETER.nodeEnableIpv6 = nc.isEnableIpv6(); + + PARAMETER.syncFetchBatchNum = nc.getSyncFetchBatchNum(); + PARAMETER.solidityThreads = nc.getSolidityThreads(); + PARAMETER.blockProducedTimeOut = nc.getBlockProducedTimeOut(); + + PARAMETER.maxHttpConnectNumber = nc.getMaxHttpConnectNumber(); + PARAMETER.netMaxTrxPerSecond = nc.getNetMaxTrxPerSecond(); + PARAMETER.tcpNettyWorkThreadNum = nc.getTcpNettyWorkThreadNum(); + PARAMETER.udpNettyWorkThreadNum = nc.getUdpNettyWorkThreadNum(); if (StringUtils.isEmpty(PARAMETER.trustNodeAddr)) { - PARAMETER.trustNodeAddr = - config.hasPath(ConfigKey.NODE_TRUST_NODE) - ? config.getString(ConfigKey.NODE_TRUST_NODE) : null; - } - - PARAMETER.validateSignThreadNum = - config.hasPath(ConfigKey.NODE_VALIDATE_SIGN_THREAD_NUM) ? config - .getInt(ConfigKey.NODE_VALIDATE_SIGN_THREAD_NUM) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.walletExtensionApi = - config.hasPath(ConfigKey.NODE_WALLET_EXTENSION_API) - && config.getBoolean(ConfigKey.NODE_WALLET_EXTENSION_API); - PARAMETER.estimateEnergy = - config.hasPath(ConfigKey.VM_ESTIMATE_ENERGY) - && config.getBoolean(ConfigKey.VM_ESTIMATE_ENERGY); - PARAMETER.estimateEnergyMaxRetry = config.hasPath(ConfigKey.VM_ESTIMATE_ENERGY_MAX_RETRY) - ? config.getInt(ConfigKey.VM_ESTIMATE_ENERGY_MAX_RETRY) : 3; - if (PARAMETER.estimateEnergyMaxRetry < 0) { - PARAMETER.estimateEnergyMaxRetry = 0; - } - if (PARAMETER.estimateEnergyMaxRetry > 10) { - PARAMETER.estimateEnergyMaxRetry = 10; + String trustNode = nc.getTrustNode(); + PARAMETER.trustNodeAddr = StringUtils.isEmpty(trustNode) ? null : trustNode; } - PARAMETER.receiveTcpMinDataLength = config.hasPath(ConfigKey.NODE_RECEIVE_TCP_MIN_DATA_LENGTH) - ? config.getLong(ConfigKey.NODE_RECEIVE_TCP_MIN_DATA_LENGTH) : 2048; + PARAMETER.validateSignThreadNum = nc.getValidateSignThreadNum(); + PARAMETER.walletExtensionApi = nc.isWalletExtensionApi(); + PARAMETER.receiveTcpMinDataLength = nc.getReceiveTcpMinDataLength(); + PARAMETER.isOpenFullTcpDisconnect = nc.isOpenFullTcpDisconnect(); + PARAMETER.nodeDetectEnable = nc.isNodeDetectEnable(); - PARAMETER.isOpenFullTcpDisconnect = config.hasPath(ConfigKey.NODE_IS_OPEN_FULL_TCP_DISCONNECT) - && config.getBoolean(ConfigKey.NODE_IS_OPEN_FULL_TCP_DISCONNECT); + PARAMETER.inactiveThreshold = nc.getInactiveThreshold(); - PARAMETER.nodeDetectEnable = config.hasPath(ConfigKey.NODE_DETECT_ENABLE) - && config.getBoolean(ConfigKey.NODE_DETECT_ENABLE); + PARAMETER.maxTransactionPendingSize = nc.getMaxTransactionPendingSize(); + PARAMETER.pendingTransactionTimeout = nc.getPendingTransactionTimeout(); - PARAMETER.inactiveThreshold = config.hasPath(ConfigKey.NODE_INACTIVE_THRESHOLD) - ? config.getInt(ConfigKey.NODE_INACTIVE_THRESHOLD) : 600; - if (PARAMETER.inactiveThreshold < 1) { - PARAMETER.inactiveThreshold = 1; - } + PARAMETER.validContractProtoThreadNum = nc.getValidContractProtoThreads(); - PARAMETER.maxTransactionPendingSize = - config.hasPath(ConfigKey.NODE_MAX_TRANSACTION_PENDING_SIZE) - ? config.getInt(ConfigKey.NODE_MAX_TRANSACTION_PENDING_SIZE) : 2000; + PARAMETER.maxFastForwardNum = nc.getMaxFastForwardNum(); + PARAMETER.shieldedTransInPendingMaxCounts = nc.getShieldedTransInPendingMaxCounts(); + PARAMETER.agreeNodeCount = nc.getAgreeNodeCount(); - PARAMETER.pendingTransactionTimeout = config.hasPath(ConfigKey.NODE_PENDING_TRANSACTION_TIMEOUT) - ? config.getLong(ConfigKey.NODE_PENDING_TRANSACTION_TIMEOUT) : 60_000; + PARAMETER.setOpenHistoryQueryWhenLiteFN(nc.isOpenHistoryQueryWhenLiteFN()); + PARAMETER.nodeMetricsEnable = nc.isMetricsEnable(); + PARAMETER.openPrintLog = nc.isOpenPrintLog(); + PARAMETER.openTransactionSort = nc.isOpenTransactionSort(); + PARAMETER.blockCacheTimeout = nc.getBlockCacheTimeout(); + PARAMETER.zenTokenId = nc.getZenTokenId(); + PARAMETER.allowShieldedTransactionApi = nc.isAllowShieldedTransactionApi(); - PARAMETER.needToUpdateAsset = - !config.hasPath(ConfigKey.STORAGE_NEEDTO_UPDATE_ASSET) || config - .getBoolean(ConfigKey.STORAGE_NEEDTO_UPDATE_ASSET); - PARAMETER.trxReferenceBlock = config.hasPath(ConfigKey.TRX_REFERENCE_BLOCK) - ? config.getString(ConfigKey.TRX_REFERENCE_BLOCK) : "solid"; + PARAMETER.unsolidifiedBlockCheck = nc.isUnsolidifiedBlockCheck(); + PARAMETER.maxUnsolidifiedBlocks = nc.getMaxUnsolidifiedBlocks(); - PARAMETER.trxExpirationTimeInMilliseconds = - config.hasPath(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) - && config.getLong(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) > 0 - ? config.getLong(ConfigKey.TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS) - : Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME; + // disabledApi list — lowercase normalization + PARAMETER.disabledApiList = nc.getDisabledApi().isEmpty() + ? Collections.emptyList() + : nc.getDisabledApi().stream().map(String::toLowerCase) + .collect(Collectors.toList()); - PARAMETER.minEffectiveConnection = config.hasPath(ConfigKey.NODE_RPC_MIN_EFFECTIVE_CONNECTION) - ? config.getInt(ConfigKey.NODE_RPC_MIN_EFFECTIVE_CONNECTION) : 1; + // ---- Fields previously scattered in applyConfigParams ---- - PARAMETER.trxCacheEnable = config.hasPath(ConfigKey.NODE_RPC_TRX_CACHE_ENABLE) - && config.getBoolean(ConfigKey.NODE_RPC_TRX_CACHE_ENABLE); + // discovery (dot-notation, read in NodeConfig.fromConfig) + PARAMETER.nodeDiscoveryEnable = nc.isDiscoveryEnable(); + PARAMETER.nodeDiscoveryPersist = nc.isDiscoveryPersist(); + PARAMETER.nodeChannelReadTimeout = nc.getChannelReadTimeout(); - PARAMETER.blockNumForEnergyLimit = config.hasPath(ConfigKey.ENERGY_LIMIT_BLOCK_NUM) - ? config.getInt(ConfigKey.ENERGY_LIMIT_BLOCK_NUM) : 4727890L; + // Legacy maxActiveNodes fallback handled in NodeConfig.fromConfig() - PARAMETER.vmTrace = - config.hasPath(ConfigKey.VM_TRACE) && config.getBoolean(ConfigKey.VM_TRACE); - - PARAMETER.saveInternalTx = - config.hasPath(ConfigKey.VM_SAVE_INTERNAL_TX) - && config.getBoolean(ConfigKey.VM_SAVE_INTERNAL_TX); - - PARAMETER.saveFeaturedInternalTx = - config.hasPath(ConfigKey.VM_SAVE_FEATURED_INTERNAL_TX) - && config.getBoolean(ConfigKey.VM_SAVE_FEATURED_INTERNAL_TX); + // p2p config and external IP + PARAMETER.p2pConfig = new P2pConfig(); + PARAMETER.nodeLanIp = PARAMETER.p2pConfig.getLanIp(); + externalIp(nc); - if (!PARAMETER.saveCancelAllUnfreezeV2Details - && config.hasPath(ConfigKey.VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS)) { - PARAMETER.saveCancelAllUnfreezeV2Details = - config.getBoolean(ConfigKey.VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS); - } + // DNS publish config + PARAMETER.dnsPublishConfig = loadDnsPublishConfig(nc); - if (PARAMETER.saveCancelAllUnfreezeV2Details - && (!PARAMETER.saveInternalTx || !PARAMETER.saveFeaturedInternalTx)) { - logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as " - + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); - } + // Shielded transaction API — legacy fallback handled in NodeConfig.fromConfig() + PARAMETER.allowShieldedTransactionApi = nc.isAllowShieldedTransactionApi(); - // PARAMETER.allowShieldedTransaction = - // config.hasPath(Constant.COMMITTEE_ALLOW_SHIELDED_TRANSACTION) ? config - // .getInt(Constant.COMMITTEE_ALLOW_SHIELDED_TRANSACTION) : 0; - - PARAMETER.allowShieldedTRC20Transaction = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION) : 0; - - PARAMETER.allowMarketTransaction = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_MARKET_TRANSACTION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_MARKET_TRANSACTION) : 0; - - PARAMETER.allowTransactionFeePool = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TRANSACTION_FEE_POOL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TRANSACTION_FEE_POOL) : 0; - - PARAMETER.allowBlackHoleOptimization = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION) : 0; - - PARAMETER.allowNewResourceModel = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_NEW_RESOURCE_MODEL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_NEW_RESOURCE_MODEL) : 0; - - PARAMETER.allowTvmIstanbul = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_ISTANBUL) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_ISTANBUL) : 0; - - PARAMETER.eventPluginConfig = - config.hasPath(ConfigKey.EVENT_SUBSCRIBE) - ? getEventPluginConfig(config) : null; - - PARAMETER.eventFilter = - config.hasPath(ConfigKey.EVENT_SUBSCRIBE_FILTER) ? getEventFilter(config) : null; - - PARAMETER.eventSubscribe = config.hasPath(ConfigKey.EVENT_SUBSCRIBE_ENABLE) - && config.getBoolean(ConfigKey.EVENT_SUBSCRIBE_ENABLE); - - if (config.hasPath(ConfigKey.ALLOW_SHIELDED_TRANSACTION_API)) { - PARAMETER.allowShieldedTransactionApi = - config.getBoolean(ConfigKey.ALLOW_SHIELDED_TRANSACTION_API); - } else if (config.hasPath(ConfigKey.NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION)) { - // for compatibility with previous configuration - PARAMETER.allowShieldedTransactionApi = - config.getBoolean(ConfigKey.NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION); - logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " - + "Please use [node.allowShieldedTransactionApi] instead."); - } else { - PARAMETER.allowShieldedTransactionApi = true; + // Active/passive/fastForward node lists from bean with filtering + PARAMETER.activeNodes = filterInetSocketAddress(nc.getActive(), true); + PARAMETER.passiveNodes = new ArrayList<>(); + for (InetSocketAddress sa : filterInetSocketAddress(nc.getPassive(), false)) { + PARAMETER.passiveNodes.add(sa.getAddress()); } + PARAMETER.fastForwardNodes = filterInetSocketAddress(nc.getFastForward(), true); - PARAMETER.zenTokenId = config.hasPath(ConfigKey.NODE_ZEN_TOKENID) - ? config.getString(ConfigKey.NODE_ZEN_TOKENID) : "000000"; - - PARAMETER.allowProtoFilterNum = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_PROTO_FILTER_NUM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_PROTO_FILTER_NUM) : 0; - - PARAMETER.allowAccountStateRoot = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT) : 0; - - PARAMETER.validContractProtoThreadNum = - config.hasPath(ConfigKey.NODE_VALID_CONTRACT_PROTO_THREADS) ? config - .getInt(ConfigKey.NODE_VALID_CONTRACT_PROTO_THREADS) - : Runtime.getRuntime().availableProcessors(); - - PARAMETER.activeNodes = getInetSocketAddress(config, ConfigKey.NODE_ACTIVE, true); - - PARAMETER.passiveNodes = getInetAddress(config, ConfigKey.NODE_PASSIVE); - - PARAMETER.fastForwardNodes = getInetSocketAddress(config, ConfigKey.NODE_FAST_FORWARD, true); - - PARAMETER.maxFastForwardNum = config.hasPath(ConfigKey.NODE_MAX_FAST_FORWARD_NUM) ? config - .getInt(ConfigKey.NODE_MAX_FAST_FORWARD_NUM) : 4; - if (PARAMETER.maxFastForwardNum > MAX_ACTIVE_WITNESS_NUM) { - PARAMETER.maxFastForwardNum = MAX_ACTIVE_WITNESS_NUM; - } - if (PARAMETER.maxFastForwardNum < 1) { - PARAMETER.maxFastForwardNum = 1; + // node.shutdown from bean (dot-notation, read in NodeConfig.fromConfig) + String shutdownBlockTime = nc.getShutdownBlockTime(); + if (!shutdownBlockTime.isEmpty()) { + try { + PARAMETER.shutdownBlockTime = new CronExpression(shutdownBlockTime); + } catch (ParseException e) { + throw new TronError(e, TronError.ErrCode.AUTO_STOP_PARAMS); + } } - - PARAMETER.shieldedTransInPendingMaxCounts = - config.hasPath(ConfigKey.NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS) ? config - .getInt(ConfigKey.NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS) : 10; - - PARAMETER.rateLimiterGlobalQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_QPS) : 50000; - - PARAMETER.rateLimiterGlobalIpQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_IP_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_IP_QPS) : 10000; - - PARAMETER.rateLimiterGlobalApiQps = - config.hasPath(ConfigKey.RATE_LIMITER_GLOBAL_API_QPS) ? config - .getInt(ConfigKey.RATE_LIMITER_GLOBAL_API_QPS) : 1000; - - PARAMETER.rateLimiterInitialization = getRateLimiterFromConfig(config); - - PARAMETER.rateLimiterSyncBlockChain = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN) : 3.0; - - PARAMETER.rateLimiterFetchInvData = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_FETCH_INV_DATA) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_FETCH_INV_DATA) : 3.0; - - PARAMETER.rateLimiterDisconnect = - config.hasPath(ConfigKey.RATE_LIMITER_P2P_DISCONNECT) ? config - .getDouble(ConfigKey.RATE_LIMITER_P2P_DISCONNECT) : 1.0; - - PARAMETER.changedDelegation = - config.hasPath(ConfigKey.COMMITTEE_CHANGED_DELEGATION) ? config - .getInt(ConfigKey.COMMITTEE_CHANGED_DELEGATION) : 0; - - PARAMETER.allowPBFT = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_PBFT) ? config - .getLong(ConfigKey.COMMITTEE_ALLOW_PBFT) : 0; - - PARAMETER.pBFTExpireNum = - config.hasPath(ConfigKey.COMMITTEE_PBFT_EXPIRE_NUM) ? config - .getLong(ConfigKey.COMMITTEE_PBFT_EXPIRE_NUM) : 20; - - PARAMETER.agreeNodeCount = config.hasPath(ConfigKey.NODE_AGREE_NODE_COUNT) ? config - .getInt(ConfigKey.NODE_AGREE_NODE_COUNT) : MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; - PARAMETER.agreeNodeCount = PARAMETER.agreeNodeCount > MAX_ACTIVE_WITNESS_NUM - ? MAX_ACTIVE_WITNESS_NUM : PARAMETER.agreeNodeCount; - if (PARAMETER.isWitness()) { - // INSTANCE.agreeNodeCount = MAX_ACTIVE_WITNESS_NUM * 2 / 3 + 1; + if (nc.getShutdownBlockHeight() >= 0) { + PARAMETER.shutdownBlockHeight = nc.getShutdownBlockHeight(); } - - PARAMETER.allowTvmFreeze = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_FREEZE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_FREEZE) : 0; - - PARAMETER.allowTvmVote = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_VOTE) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_VOTE) : 0; - - PARAMETER.allowTvmLondon = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_LONDON) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_LONDON) : 0; - - PARAMETER.allowTvmCompatibleEvm = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM) : 0; - - PARAMETER.allowHigherLimitForMaxCpuTimeOfOneTx = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX) : 0; - - PARAMETER.allowNewRewardAlgorithm = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM) : 0; - - PARAMETER.allowOptimizedReturnValueOfChainId = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID) : 0; - - initBackupProperty(config); - initRocksDbBackupProperty(config); - initRocksDbSettings(config); - - PARAMETER.actuatorSet = - config.hasPath(ConfigKey.ACTUATOR_WHITELIST) - ? new HashSet<>(config.getStringList(ConfigKey.ACTUATOR_WHITELIST)) - : Collections.emptySet(); - - if (config.hasPath(ConfigKey.NODE_METRICS_ENABLE)) { - PARAMETER.nodeMetricsEnable = config.getBoolean(ConfigKey.NODE_METRICS_ENABLE); + if (nc.getShutdownBlockCount() >= 0) { + PARAMETER.shutdownBlockCount = nc.getShutdownBlockCount(); } + } - PARAMETER.metricsStorageEnable = config.hasPath(ConfigKey.METRICS_STORAGE_ENABLE) && config - .getBoolean(ConfigKey.METRICS_STORAGE_ENABLE); - PARAMETER.influxDbIp = config.hasPath(ConfigKey.METRICS_INFLUXDB_IP) ? config - .getString(ConfigKey.METRICS_INFLUXDB_IP) : Constant.LOCAL_HOST; - PARAMETER.influxDbPort = config.hasPath(ConfigKey.METRICS_INFLUXDB_PORT) ? config - .getInt(ConfigKey.METRICS_INFLUXDB_PORT) : 8086; - PARAMETER.influxDbDatabase = config.hasPath(ConfigKey.METRICS_INFLUXDB_DATABASE) ? config - .getString(ConfigKey.METRICS_INFLUXDB_DATABASE) : "metrics"; - PARAMETER.metricsReportInterval = config.hasPath(ConfigKey.METRICS_REPORT_INTERVAL) ? config - .getInt(ConfigKey.METRICS_REPORT_INTERVAL) : 10; - - PARAMETER.metricsPrometheusEnable = - config.hasPath(ConfigKey.METRICS_PROMETHEUS_ENABLE) - && config.getBoolean(ConfigKey.METRICS_PROMETHEUS_ENABLE); - PARAMETER.metricsPrometheusPort = config.hasPath(ConfigKey.METRICS_PROMETHEUS_PORT) ? config - .getInt(ConfigKey.METRICS_PROMETHEUS_PORT) : 9527; - PARAMETER.setOpenHistoryQueryWhenLiteFN( - config.hasPath(ConfigKey.NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN) - && config.getBoolean(ConfigKey.NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN)); - - PARAMETER.historyBalanceLookup = config.hasPath(ConfigKey.HISTORY_BALANCE_LOOKUP) && config - .getBoolean(ConfigKey.HISTORY_BALANCE_LOOKUP); - - if (config.hasPath(ConfigKey.OPEN_PRINT_LOG)) { - PARAMETER.openPrintLog = config.getBoolean(ConfigKey.OPEN_PRINT_LOG); + /** + * Apply platform-specific constraints after all config sources are resolved. + * ARM64 does not support LevelDB (native JNI library unavailable), + * so db.engine is forced to RocksDB regardless of config or CLI settings. + */ + private static void applyPlatformConstraints() { + if (Arch.isArm64() + && !Constant.ROCKSDB.equalsIgnoreCase(PARAMETER.storage.getDbEngine())) { + logger.warn("ARM64 only supports RocksDB, ignoring db.engine='{}'", + PARAMETER.storage.getDbEngine()); + PARAMETER.storage.setDbEngine(Constant.ROCKSDB); } + } - PARAMETER.openTransactionSort = config.hasPath(ConfigKey.OPEN_TRANSACTION_SORT) && config - .getBoolean(ConfigKey.OPEN_TRANSACTION_SORT); - - PARAMETER.allowAccountAssetOptimization = config - .hasPath(ConfigKey.ALLOW_ACCOUNT_ASSET_OPTIMIZATION) ? config - .getInt(ConfigKey.ALLOW_ACCOUNT_ASSET_OPTIMIZATION) : 0; - - PARAMETER.allowAssetOptimization = config - .hasPath(ConfigKey.ALLOW_ASSET_OPTIMIZATION) ? config - .getInt(ConfigKey.ALLOW_ASSET_OPTIMIZATION) : 0; + /** + * Apply parameters from config file. + */ + public static void applyConfigParams( + final Config config) { - PARAMETER.disabledApiList = - config.hasPath(ConfigKey.NODE_DISABLED_API_LIST) - ? config.getStringList(ConfigKey.NODE_DISABLED_API_LIST) - .stream().map(String::toLowerCase).collect(Collectors.toList()) - : Collections.emptyList(); + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + Wallet.setAddressPreFixString(Constant.ADD_PRE_FIX_STRING_MAINNET); - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)) { - try { - PARAMETER.shutdownBlockTime = new CronExpression(config.getString( - ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)); - } catch (ParseException e) { - throw new TronError(e, TronError.ErrCode.AUTO_STOP_PARAMS); - } - } + // crypto.engine handled by MiscConfig - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_HEIGHT)) { - PARAMETER.shutdownBlockHeight = config.getLong(ConfigKey.NODE_SHUTDOWN_BLOCK_HEIGHT); - } + // VM config: bind from config.conf "vm" section + vmConfig = VmConfig.fromConfig(config); + applyVmConfig(vmConfig); - if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_COUNT)) { - PARAMETER.shutdownBlockCount = config.getLong(ConfigKey.NODE_SHUTDOWN_BLOCK_COUNT); - } + // Node config: bind from config.conf "node" section + nodeConfig = NodeConfig.fromConfig(config); + applyNodeConfig(nodeConfig); - if (config.hasPath(ConfigKey.BLOCK_CACHE_TIMEOUT)) { - PARAMETER.blockCacheTimeout = config.getLong(ConfigKey.BLOCK_CACHE_TIMEOUT); - } + // vm.minTimeRatio, vm.maxTimeRatio, vm.longRunningTime already handled by VmConfig above - if (config.hasPath(ConfigKey.ALLOW_NEW_REWARD)) { - PARAMETER.allowNewReward = config.getLong(ConfigKey.ALLOW_NEW_REWARD); - if (PARAMETER.allowNewReward > 1) { - PARAMETER.allowNewReward = 1; - } - if (PARAMETER.allowNewReward < 0) { - PARAMETER.allowNewReward = 0; - } - } + // Storage config: bind from config.conf "storage" section + PARAMETER.storage = new Storage(); + storageConfig = StorageConfig.fromConfig(config); + applyStorageConfig(storageConfig); - if (config.hasPath(ConfigKey.MEMO_FEE)) { - PARAMETER.memoFee = config.getLong(ConfigKey.MEMO_FEE); - if (PARAMETER.memoFee > 1_000_000_000) { - PARAMETER.memoFee = 1_000_000_000; - } - if (PARAMETER.memoFee < 0) { - PARAMETER.memoFee = 0; - } - } + // seed.node is a top-level config section (not under "node") — config structure + // is arguably misplaced, but preserved for backward compatibility - if (config.hasPath(ConfigKey.ALLOW_DELEGATE_OPTIMIZATION)) { - PARAMETER.allowDelegateOptimization = config.getLong(ConfigKey.ALLOW_DELEGATE_OPTIMIZATION); - PARAMETER.allowDelegateOptimization = min(PARAMETER.allowDelegateOptimization, 1, true); - PARAMETER.allowDelegateOptimization = max(PARAMETER.allowDelegateOptimization, 0, true); - } + // Genesis config: bind from config.conf "genesis.block" section + genesisConfig = GenesisConfig.fromConfig(config); + applyGenesisConfig(genesisConfig, config); - if (config.hasPath(ConfigKey.COMMITTEE_UNFREEZE_DELAY_DAYS)) { - PARAMETER.unfreezeDelayDays = config.getLong(ConfigKey.COMMITTEE_UNFREEZE_DELAY_DAYS); - if (PARAMETER.unfreezeDelayDays > 365) { - PARAMETER.unfreezeDelayDays = 365; - } - if (PARAMETER.unfreezeDelayDays < 0) { - PARAMETER.unfreezeDelayDays = 0; - } - } + // Block config: bind from config.conf "block" section + blockConfig = BlockConfig.fromConfig(config); + applyBlockConfig(blockConfig); - if (config.hasPath(ConfigKey.ALLOW_DYNAMIC_ENERGY)) { - PARAMETER.allowDynamicEnergy = config.getLong(ConfigKey.ALLOW_DYNAMIC_ENERGY); - PARAMETER.allowDynamicEnergy = min(PARAMETER.allowDynamicEnergy, 1, true); - PARAMETER.allowDynamicEnergy = max(PARAMETER.allowDynamicEnergy, 0, true); - } + // node discovery, legacy fallback, p2p, dns — all handled in applyNodeConfig - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_THRESHOLD)) { - PARAMETER.dynamicEnergyThreshold = config.getLong(ConfigKey.DYNAMIC_ENERGY_THRESHOLD); - PARAMETER.dynamicEnergyThreshold - = min(PARAMETER.dynamicEnergyThreshold, 100_000_000_000_000_000L, true); - PARAMETER.dynamicEnergyThreshold = max(PARAMETER.dynamicEnergyThreshold, 0, true); - } + // Misc config: storage, trx, energy — small domains, read via beans + miscConfig = MiscConfig.fromConfig(config); + applyMiscConfig(miscConfig); - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_INCREASE_FACTOR)) { - PARAMETER.dynamicEnergyIncreaseFactor - = config.getLong(ConfigKey.DYNAMIC_ENERGY_INCREASE_FACTOR); - PARAMETER.dynamicEnergyIncreaseFactor = - min(PARAMETER.dynamicEnergyIncreaseFactor, DYNAMIC_ENERGY_INCREASE_FACTOR_RANGE, true); - PARAMETER.dynamicEnergyIncreaseFactor = max(PARAMETER.dynamicEnergyIncreaseFactor, 0, true); - } + // vm, committee already handled above - if (config.hasPath(ConfigKey.DYNAMIC_ENERGY_MAX_FACTOR)) { - PARAMETER.dynamicEnergyMaxFactor - = config.getLong(ConfigKey.DYNAMIC_ENERGY_MAX_FACTOR); - PARAMETER.dynamicEnergyMaxFactor = - min(PARAMETER.dynamicEnergyMaxFactor, DYNAMIC_ENERGY_MAX_FACTOR_RANGE, true); - PARAMETER.dynamicEnergyMaxFactor = max(PARAMETER.dynamicEnergyMaxFactor, 0, true); - } + // Committee config: bind from config.conf "committee" section + committeeConfig = CommitteeConfig.fromConfig(config); + applyCommitteeConfig(committeeConfig); - PARAMETER.dynamicConfigEnable = config.hasPath(ConfigKey.DYNAMIC_CONFIG_ENABLE) - && config.getBoolean(ConfigKey.DYNAMIC_CONFIG_ENABLE); - if (config.hasPath(ConfigKey.DYNAMIC_CONFIG_CHECK_INTERVAL)) { - PARAMETER.dynamicConfigCheckInterval - = config.getLong(ConfigKey.DYNAMIC_CONFIG_CHECK_INTERVAL); - if (PARAMETER.dynamicConfigCheckInterval <= 0) { - PARAMETER.dynamicConfigCheckInterval = 600; - } - } else { - PARAMETER.dynamicConfigCheckInterval = 600; - } + // shielded transaction API, active/passive/fastForward — handled in applyNodeConfig - PARAMETER.allowTvmShangHai = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_SHANGHAI) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_SHANGHAI) : 0; - - PARAMETER.unsolidifiedBlockCheck = - config.hasPath(ConfigKey.UNSOLIDIFIED_BLOCK_CHECK) - && config.getBoolean(ConfigKey.UNSOLIDIFIED_BLOCK_CHECK); - - PARAMETER.maxUnsolidifiedBlocks = - config.hasPath(ConfigKey.MAX_UNSOLIDIFIED_BLOCKS) ? config - .getInt(ConfigKey.MAX_UNSOLIDIFIED_BLOCKS) : 54; - - long allowOldRewardOpt = config.hasPath(ConfigKey.COMMITTEE_ALLOW_OLD_REWARD_OPT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_OLD_REWARD_OPT) : 0; - if (allowOldRewardOpt == 1 && PARAMETER.allowNewRewardAlgorithm != 1 - && PARAMETER.allowNewReward != 1 && PARAMETER.allowTvmVote != 1) { - throw new IllegalArgumentException( - "At least one of the following proposals is required to be opened first: " - + "committee.allowNewRewardAlgorithm = 1" - + " or committee.allowNewReward = 1" - + " or committee.allowTvmVote = 1."); - } - PARAMETER.allowOldRewardOpt = allowOldRewardOpt; + // Rate limiter config: bind from config.conf "rate.limiter" section + rateLimiterConfig = RateLimiterConfig.fromConfig(config); + applyRateLimiterConfig(rateLimiterConfig); - PARAMETER.allowEnergyAdjustment = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_ENERGY_ADJUSTMENT) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_ENERGY_ADJUSTMENT) : 0; + // Node backup: from NodeConfig bean + applyNodeBackupConfig(nodeConfig); - PARAMETER.allowStrictMath = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_STRICT_MATH) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_STRICT_MATH) : 0; + // actuatorSet already set in applyMiscConfig - PARAMETER.consensusLogicOptimization = - config.hasPath(ConfigKey.COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION) ? config - .getInt(ConfigKey.COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION) : 0; + // Metrics config: bind from config.conf "node.metrics" section + metricsConfig = MetricsConfig.fromConfig(config); + applyMetricsConfig(metricsConfig); - PARAMETER.allowTvmCancun = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_CANCUN) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_CANCUN) : 0; + // historyBalanceLookup already handled by MiscConfig above - PARAMETER.allowTvmBlob = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_BLOB) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_BLOB) : 0; + // node.shutdown — handled in applyNodeConfig - PARAMETER.allowTvmOsaka = - config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) ? config - .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) : 0; + // Event config: bind from config.conf "event.subscribe" section + eventConfig = EventConfig.fromConfig(config); + applyEventConfig(eventConfig); logConfig(); } @@ -1198,26 +957,20 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { return; } - String witnessAddr = config.hasPath(ConfigKey.LOCAL_WITNESS_ACCOUNT_ADDRESS) - ? config.getString(ConfigKey.LOCAL_WITNESS_ACCOUNT_ADDRESS) : null; + LocalWitnessConfig lwConfig = LocalWitnessConfig.fromConfig(config); // path 2: config localwitness (private key list) - if (config.hasPath(ConfigKey.LOCAL_WITNESS)) { - List keys = config.getStringList(ConfigKey.LOCAL_WITNESS); - if (!keys.isEmpty()) { - localWitnesses = WitnessInitializer.initFromCFGPrivateKey(keys, witnessAddr); - return; - } + if (!lwConfig.getPrivateKeys().isEmpty()) { + localWitnesses = WitnessInitializer.initFromCFGPrivateKey( + lwConfig.getPrivateKeys(), lwConfig.getAccountAddress()); + return; } // path 3: config localwitnesskeystore + password - if (config.hasPath(ConfigKey.LOCAL_WITNESS_KEYSTORE)) { - List keystores = config.getStringList(ConfigKey.LOCAL_WITNESS_KEYSTORE); - if (!keystores.isEmpty()) { - localWitnesses = WitnessInitializer.initFromKeystore( - keystores, cmd.password, witnessAddr); - return; - } + if (!lwConfig.getKeystores().isEmpty()) { + localWitnesses = WitnessInitializer.initFromKeystore( + lwConfig.getKeystores(), cmd.password, lwConfig.getAccountAddress()); + return; } // no private key source configured @@ -1230,85 +983,35 @@ public static void clearParam() { CommonParameter.reset(); configFilePath = ""; localWitnesses = null; + nodeConfig = null; + vmConfig = null; + blockConfig = null; + committeeConfig = null; + storageConfig = null; + genesisConfig = null; + miscConfig = null; + rateLimiterConfig = null; + metricsConfig = null; + eventConfig = null; } - private static long getProposalExpirationTime(final Config config) { - if (config.hasPath(ConfigKey.COMMITTEE_PROPOSAL_EXPIRE_TIME)) { - throw new TronError("It is not allowed to configure committee.proposalExpireTime in " - + "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT); - } - if (config.hasPath(ConfigKey.BLOCK_PROPOSAL_EXPIRE_TIME)) { - long proposalExpireTime = config.getLong(ConfigKey.BLOCK_PROPOSAL_EXPIRE_TIME); - if (proposalExpireTime <= MIN_PROPOSAL_EXPIRE_TIME - || proposalExpireTime >= MAX_PROPOSAL_EXPIRE_TIME) { - throw new TronError("The value[block.proposalExpireTime] is only allowed to " - + "be greater than " + MIN_PROPOSAL_EXPIRE_TIME + " and less than " - + MAX_PROPOSAL_EXPIRE_TIME + "!", PARAMETER_INIT); - } - return proposalExpireTime; - } else { - return DEFAULT_PROPOSAL_EXPIRE_TIME; - } - } - - private static List getWitnessesFromConfig(final com.typesafe.config.Config config) { - return config.getObjectList(ConfigKey.GENESIS_BLOCK_WITNESSES).stream() - .map(Args::createWitness) - .collect(Collectors.toCollection(ArrayList::new)); - } - - private static Witness createWitness(final ConfigObject witnessAccount) { - final Witness witness = new Witness(); - witness.setAddress( - Commons.decodeFromBase58Check(witnessAccount.get("address").unwrapped().toString())); - witness.setUrl(witnessAccount.get("url").unwrapped().toString()); - witness.setVoteCount(witnessAccount.toConfig().getLong("voteCount")); - return witness; - } + // getProposalExpirationTime removed — logic moved to BlockConfig.fromConfig() - private static List getAccountsFromConfig(final com.typesafe.config.Config config) { - return config.getObjectList(ConfigKey.GENESIS_BLOCK_ASSETS).stream() - .map(Args::createAccount) - .collect(Collectors.toCollection(ArrayList::new)); - } + // getWitnessesFromConfig, createWitness, getAccountsFromConfig, createAccount + // removed — logic moved to applyGenesisConfig() - private static Account createAccount(final ConfigObject asset) { - final Account account = new Account(); - account.setAccountName(asset.get("accountName").unwrapped().toString()); - account.setAccountType(asset.get("accountType").unwrapped().toString()); - account.setAddress(Commons.decodeFromBase58Check(asset.get("address").unwrapped().toString())); - account.setBalance(asset.get("balance").unwrapped().toString()); - return account; - } + // getRateLimiterFromConfig removed — logic moved to applyRateLimiterConfig() - private static RateLimiterInitialization getRateLimiterFromConfig( - final com.typesafe.config.Config config) { - RateLimiterInitialization initialization = new RateLimiterInitialization(); - if (config.hasPath(ConfigKey.RATE_LIMITER_HTTP)) { - ArrayList list1 = config - .getObjectList(ConfigKey.RATE_LIMITER_HTTP).stream() - .map(RateLimiterInitialization::createHttpItem) - .collect(Collectors.toCollection(ArrayList::new)); - initialization.setHttpMap(list1); - } - if (config.hasPath(ConfigKey.RATE_LIMITER_RPC)) { - ArrayList list2 = config - .getObjectList(ConfigKey.RATE_LIMITER_RPC).stream() - .map(RateLimiterInitialization::createRpcItem) - .collect(Collectors.toCollection(ArrayList::new)); - initialization.setRpcMap(list2); - } - return initialization; - } + // getInetSocketAddress removed — use filterInetSocketAddress - public static List getInetSocketAddress( - final com.typesafe.config.Config config, String path, boolean filter) { + /** + * Parse and optionally filter a list of address strings. + * Overload that accepts a pre-read list from a bean instead of a config path. + */ + public static List filterInetSocketAddress( + List addressList, boolean filter) { List ret = new ArrayList<>(); - if (!config.hasPath(path)) { - return ret; - } - List list = config.getStringList(path); - for (String configString : list) { + for (String configString : addressList) { InetSocketAddress inetSocketAddress = NetUtil.parseInetSocketAddress(configString); if (filter) { String ip = inetSocketAddress.getAddress().getHostAddress(); @@ -1326,149 +1029,72 @@ public static List getInetSocketAddress( return ret; } - public static List getInetAddress( - final com.typesafe.config.Config config, String path) { - List ret = new ArrayList<>(); - if (!config.hasPath(path)) { - return ret; - } - List list = config.getStringList(path); - for (String configString : list) { - InetSocketAddress inetSocketAddress = NetUtil.parseInetSocketAddress(configString); - ret.add(inetSocketAddress.getAddress()); - } - return ret; - } + // getInetAddress removed — use filterInetSocketAddress - private static EventPluginConfig getEventPluginConfig( - final com.typesafe.config.Config config) { - EventPluginConfig eventPluginConfig = new EventPluginConfig(); + // getEventPluginConfig removed — logic moved to applyEventConfig() - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_VERSION)) { - eventPluginConfig.setVersion(config.getInt(ConfigKey.EVENT_SUBSCRIBE_VERSION)); - } - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM)) { - eventPluginConfig.setStartSyncBlockNum(config - .getLong(ConfigKey.EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM)); - } - - boolean useNativeQueue = false; - int bindPort = 0; - int sendQueueLength = 0; - if (config.hasPath(ConfigKey.USE_NATIVE_QUEUE)) { - useNativeQueue = config.getBoolean(ConfigKey.USE_NATIVE_QUEUE); - - if (config.hasPath(ConfigKey.NATIVE_QUEUE_BIND_PORT)) { - bindPort = config.getInt(ConfigKey.NATIVE_QUEUE_BIND_PORT); - } - - if (config.hasPath(ConfigKey.NATIVE_QUEUE_SEND_LENGTH)) { - sendQueueLength = config.getInt(ConfigKey.NATIVE_QUEUE_SEND_LENGTH); - } - - eventPluginConfig.setUseNativeQueue(useNativeQueue); - eventPluginConfig.setBindPort(bindPort); - eventPluginConfig.setSendQueueLength(sendQueueLength); - } - - // use event plugin - if (!useNativeQueue) { - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_PATH)) { - String pluginPath = config.getString(ConfigKey.EVENT_SUBSCRIBE_PATH); - if (StringUtils.isNotEmpty(pluginPath)) { - eventPluginConfig.setPluginPath(pluginPath.trim()); - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_SERVER)) { - String serverAddress = config.getString(ConfigKey.EVENT_SUBSCRIBE_SERVER); - if (StringUtils.isNotEmpty(serverAddress)) { - eventPluginConfig.setServerAddress(serverAddress.trim()); - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_DB_CONFIG)) { - String dbConfig = config.getString(ConfigKey.EVENT_SUBSCRIBE_DB_CONFIG); - if (StringUtils.isNotEmpty(dbConfig)) { - eventPluginConfig.setDbConfig(dbConfig.trim()); - } - } - } - - if (config.hasPath(ConfigKey.EVENT_SUBSCRIBE_TOPICS)) { - List triggerConfigList = config.getObjectList(ConfigKey.EVENT_SUBSCRIBE_TOPICS) - .stream() - .map(Args::createTriggerConfig) - .collect(Collectors.toCollection(ArrayList::new)); - - eventPluginConfig.setTriggerConfigList(triggerConfigList); - } - - return eventPluginConfig; - } - - - public static PublishConfig loadDnsPublishConfig(final com.typesafe.config.Config config) { + public static PublishConfig loadDnsPublishConfig(NodeConfig nodeConfig) { PublishConfig publishConfig = new PublishConfig(); - if (config.hasPath(ConfigKey.NODE_DNS_PUBLISH)) { - publishConfig.setDnsPublishEnable(config.getBoolean(ConfigKey.NODE_DNS_PUBLISH)); - } - loadDnsPublishParameters(config, publishConfig); + NodeConfig.DnsConfig dns = nodeConfig.getDns(); + publishConfig.setDnsPublishEnable(dns.isPublish()); + loadDnsPublishParameters(dns, publishConfig); return publishConfig; } + /** + * Load DNS publish parameters from bean into PublishConfig. + * Public method — called by tests and external code. + */ public static void loadDnsPublishParameters(final com.typesafe.config.Config config, PublishConfig publishConfig) { + NodeConfig nodeConfig = NodeConfig.fromConfig(config); + loadDnsPublishParameters(nodeConfig.getDns(), publishConfig); + } + + private static void loadDnsPublishParameters(NodeConfig.DnsConfig dns, + PublishConfig publishConfig) { + if (publishConfig.isDnsPublishEnable()) { - if (config.hasPath(ConfigKey.NODE_DNS_DOMAIN) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_DOMAIN))) { - publishConfig.setDnsDomain(config.getString(ConfigKey.NODE_DNS_DOMAIN)); + if (StringUtils.isNotEmpty(dns.getDnsDomain())) { + publishConfig.setDnsDomain(dns.getDnsDomain()); } else { - logEmptyError(ConfigKey.NODE_DNS_DOMAIN); + logEmptyError("node.dns.dnsDomain"); } - if (config.hasPath(ConfigKey.NODE_DNS_CHANGE_THRESHOLD)) { - double changeThreshold = config.getDouble(ConfigKey.NODE_DNS_CHANGE_THRESHOLD); - if (changeThreshold > 0) { - publishConfig.setChangeThreshold(changeThreshold); - } else { - logger.error("Check {}, should be bigger than 0, default 0.1", - ConfigKey.NODE_DNS_CHANGE_THRESHOLD); - } + if (dns.getChangeThreshold() > 0) { + publishConfig.setChangeThreshold(dns.getChangeThreshold()); + } else if (Double.compare(dns.getChangeThreshold(), 0.0) != 0) { + logger.error("Check node.dns.changeThreshold, should be bigger than 0, default 0.1"); } - if (config.hasPath(ConfigKey.NODE_DNS_MAX_MERGE_SIZE)) { - int maxMergeSize = config.getInt(ConfigKey.NODE_DNS_MAX_MERGE_SIZE); - if (maxMergeSize >= 1 && maxMergeSize <= 5) { - publishConfig.setMaxMergeSize(maxMergeSize); - } else { - logger.error("Check {}, should be [1~5], default 5", ConfigKey.NODE_DNS_MAX_MERGE_SIZE); - } + int maxMergeSize = dns.getMaxMergeSize(); + if (maxMergeSize >= 1 && maxMergeSize <= 5) { + publishConfig.setMaxMergeSize(maxMergeSize); + } else if (maxMergeSize != 0) { + logger.error("Check node.dns.maxMergeSize, should be [1~5], default 5"); } - if (config.hasPath(ConfigKey.NODE_DNS_PRIVATE) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_PRIVATE))) { - publishConfig.setDnsPrivate(config.getString(ConfigKey.NODE_DNS_PRIVATE)); + if (StringUtils.isNotEmpty(dns.getDnsPrivate())) { + publishConfig.setDnsPrivate(dns.getDnsPrivate()); } else { - logEmptyError(ConfigKey.NODE_DNS_PRIVATE); + logEmptyError("node.dns.dnsPrivate"); } - if (config.hasPath(ConfigKey.NODE_DNS_KNOWN_URLS)) { - publishConfig.setKnownTreeUrls(config.getStringList(ConfigKey.NODE_DNS_KNOWN_URLS)); + if (!dns.getKnownUrls().isEmpty()) { + publishConfig.setKnownTreeUrls(dns.getKnownUrls()); } - if (config.hasPath(ConfigKey.NODE_DNS_STATIC_NODES)) { + if (!dns.getStaticNodes().isEmpty()) { publishConfig.setStaticNodes( - getInetSocketAddress(config, ConfigKey.NODE_DNS_STATIC_NODES, false)); + filterInetSocketAddress(dns.getStaticNodes(), false)); } - if (config.hasPath(ConfigKey.NODE_DNS_SERVER_TYPE) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_SERVER_TYPE))) { - String serverType = config.getString(ConfigKey.NODE_DNS_SERVER_TYPE); + String serverType = dns.getServerType(); + if (StringUtils.isNotEmpty(serverType)) { if (!"aws".equalsIgnoreCase(serverType) && !"aliyun".equalsIgnoreCase(serverType)) { throw new IllegalArgumentException( - String.format("Check %s, must be aws or aliyun", ConfigKey.NODE_DNS_SERVER_TYPE)); + "Check node.dns.serverType, must be aws or aliyun"); } if ("aws".equalsIgnoreCase(serverType)) { publishConfig.setDnsType(DnsType.AwsRoute53); @@ -1476,38 +1102,34 @@ public static void loadDnsPublishParameters(final com.typesafe.config.Config con publishConfig.setDnsType(DnsType.AliYun); } } else { - logEmptyError(ConfigKey.NODE_DNS_SERVER_TYPE); + logEmptyError("node.dns.serverType"); } - if (config.hasPath(ConfigKey.NODE_DNS_ACCESS_KEY_ID) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_ID))) { - publishConfig.setAccessKeyId(config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_ID)); + if (StringUtils.isNotEmpty(dns.getAccessKeyId())) { + publishConfig.setAccessKeyId(dns.getAccessKeyId()); } else { - logEmptyError(ConfigKey.NODE_DNS_ACCESS_KEY_ID); + logEmptyError("node.dns.accessKeyId"); } - if (config.hasPath(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET))) { - publishConfig.setAccessKeySecret(config.getString(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET)); + if (StringUtils.isNotEmpty(dns.getAccessKeySecret())) { + publishConfig.setAccessKeySecret(dns.getAccessKeySecret()); } else { - logEmptyError(ConfigKey.NODE_DNS_ACCESS_KEY_SECRET); + logEmptyError("node.dns.accessKeySecret"); } if (publishConfig.getDnsType() == DnsType.AwsRoute53) { - if (config.hasPath(ConfigKey.NODE_DNS_AWS_REGION) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_AWS_REGION))) { - publishConfig.setAwsRegion(config.getString(ConfigKey.NODE_DNS_AWS_REGION)); + if (StringUtils.isNotEmpty(dns.getAwsRegion())) { + publishConfig.setAwsRegion(dns.getAwsRegion()); } else { - logEmptyError(ConfigKey.NODE_DNS_AWS_REGION); + logEmptyError("node.dns.awsRegion"); } - if (config.hasPath(ConfigKey.NODE_DNS_AWS_HOST_ZONE_ID)) { - publishConfig.setAwsHostZoneId(config.getString(ConfigKey.NODE_DNS_AWS_HOST_ZONE_ID)); + if (StringUtils.isNotEmpty(dns.getAwsHostZoneId())) { + publishConfig.setAwsHostZoneId(dns.getAwsHostZoneId()); } } else { - if (config.hasPath(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT) && StringUtils.isNotEmpty( - config.getString(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT))) { - publishConfig.setAliDnsEndpoint(config.getString(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT)); + if (StringUtils.isNotEmpty(dns.getAliyunDnsEndpoint())) { + publishConfig.setAliDnsEndpoint(dns.getAliyunDnsEndpoint()); } else { - logEmptyError(ConfigKey.NODE_DNS_ALIYUN_ENDPOINT); + logEmptyError("node.dns.aliyunDnsEndpoint"); } } } @@ -1517,80 +1139,13 @@ private static void logEmptyError(String arg) { throw new IllegalArgumentException(String.format("Check %s, must not be null or empty", arg)); } - private static TriggerConfig createTriggerConfig(ConfigObject triggerObject) { - if (Objects.isNull(triggerObject)) { - return null; - } - - TriggerConfig triggerConfig = new TriggerConfig(); - - String triggerName = triggerObject.get("triggerName").unwrapped().toString(); - triggerConfig.setTriggerName(triggerName); - - String enabled = triggerObject.get("enable").unwrapped().toString(); - triggerConfig.setEnabled("true".equalsIgnoreCase(enabled)); - - String topic = triggerObject.get("topic").unwrapped().toString(); - triggerConfig.setTopic(topic); - - if (triggerObject.containsKey("redundancy")) { - String redundancy = triggerObject.get("redundancy").unwrapped().toString(); - triggerConfig.setRedundancy("true".equalsIgnoreCase(redundancy)); - } - - if (triggerObject.containsKey("ethCompatible")) { - String ethCompatible = triggerObject.get("ethCompatible").unwrapped().toString(); - triggerConfig.setEthCompatible("true".equalsIgnoreCase(ethCompatible)); - } - - if (triggerObject.containsKey("solidified")) { - String solidified = triggerObject.get("solidified").unwrapped().toString(); - triggerConfig.setSolidified("true".equalsIgnoreCase(solidified)); - } - - return triggerConfig; - } - - private static FilterQuery getEventFilter(final com.typesafe.config.Config config) { - FilterQuery filter = new FilterQuery(); - long fromBlockLong = 0; - long toBlockLong = 0; + // createTriggerConfig removed — logic moved to applyEventConfig() + // getEventFilter removed — logic moved to applyEventConfig() - String fromBlock = config.getString(ConfigKey.EVENT_SUBSCRIBE_FROM_BLOCK).trim(); - try { - fromBlockLong = FilterQuery.parseFromBlockNumber(fromBlock); - } catch (Exception e) { - logger.error("invalid filter: fromBlockNumber: {}", fromBlock, e); - return null; - } - filter.setFromBlock(fromBlockLong); - - String toBlock = config.getString(ConfigKey.EVENT_SUBSCRIBE_TO_BLOCK).trim(); - try { - toBlockLong = FilterQuery.parseToBlockNumber(toBlock); - } catch (Exception e) { - logger.error("invalid filter: toBlockNumber: {}", toBlock, e); - return null; - } - filter.setToBlock(toBlockLong); - - List addressList = config.getStringList(ConfigKey.EVENT_SUBSCRIBE_CONTRACT_ADDRESS); - addressList = addressList.stream().filter(address -> StringUtils.isNotEmpty(address)).collect( - Collectors.toList()); - filter.setContractAddressList(addressList); - - List topicList = config.getStringList(ConfigKey.EVENT_SUBSCRIBE_CONTRACT_TOPIC); - topicList = topicList.stream().filter(top -> StringUtils.isNotEmpty(top)).collect( - Collectors.toList()); - filter.setContractTopicList(topicList); - - return filter; - } - - private static void externalIp(final com.typesafe.config.Config config) { - if (!config.hasPath(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP) || config - .getString(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP).trim().isEmpty()) { - if (PARAMETER.nodeExternalIp == null) { + private static void externalIp(NodeConfig nodeConfig) { + String externalIp = nodeConfig.getDiscoveryExternalIp(); + if (StringUtils.isEmpty(externalIp)) { + if (StringUtils.isEmpty(PARAMETER.nodeExternalIp)) { logger.info("External IP wasn't set, using ipv4 from libp2p"); PARAMETER.nodeExternalIp = PARAMETER.p2pConfig.getIp(); if (StringUtils.isEmpty(PARAMETER.nodeExternalIp)) { @@ -1598,69 +1153,12 @@ private static void externalIp(final com.typesafe.config.Config config) { } } } else { - PARAMETER.nodeExternalIp = config.getString(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP).trim(); + PARAMETER.nodeExternalIp = externalIp; } } - private static void initRocksDbSettings(Config config) { - String prefix = ConfigKey.STORAGE_DB_SETTING; - int levelNumber = config.hasPath(prefix + "levelNumber") - ? config.getInt(prefix + "levelNumber") : 7; - int compactThreads = config.hasPath(prefix + "compactThreads") - ? config.getInt(prefix + "compactThreads") - : max(Runtime.getRuntime().availableProcessors(), 1, true); - int blocksize = config.hasPath(prefix + "blocksize") - ? config.getInt(prefix + "blocksize") : 16; - long maxBytesForLevelBase = config.hasPath(prefix + "maxBytesForLevelBase") - ? config.getInt(prefix + "maxBytesForLevelBase") : 256; - double maxBytesForLevelMultiplier = config.hasPath(prefix + "maxBytesForLevelMultiplier") - ? config.getDouble(prefix + "maxBytesForLevelMultiplier") : 10; - int level0FileNumCompactionTrigger = - config.hasPath(prefix + "level0FileNumCompactionTrigger") ? config - .getInt(prefix + "level0FileNumCompactionTrigger") : 2; - long targetFileSizeBase = config.hasPath(prefix + "targetFileSizeBase") ? config - .getLong(prefix + "targetFileSizeBase") : 64; - int targetFileSizeMultiplier = config.hasPath(prefix + "targetFileSizeMultiplier") ? config - .getInt(prefix + "targetFileSizeMultiplier") : 1; - int maxOpenFiles = config.hasPath(prefix + "maxOpenFiles") - ? config.getInt(prefix + "maxOpenFiles") : 5000; - - PARAMETER.rocksDBCustomSettings = RocksDbSettings - .initCustomSettings(levelNumber, compactThreads, blocksize, maxBytesForLevelBase, - maxBytesForLevelMultiplier, level0FileNumCompactionTrigger, - targetFileSizeBase, targetFileSizeMultiplier, maxOpenFiles); - RocksDbSettings.loggingSettings(); - } - - private static void initRocksDbBackupProperty(Config config) { - boolean enable = - config.hasPath(ConfigKey.STORAGE_BACKUP_ENABLE) - && config.getBoolean(ConfigKey.STORAGE_BACKUP_ENABLE); - String propPath = config.hasPath(ConfigKey.STORAGE_BACKUP_PROP_PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_PROP_PATH) : "prop.properties"; - String bak1path = config.hasPath(ConfigKey.STORAGE_BACKUP_BAK1PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_BAK1PATH) : "bak1/database/"; - String bak2path = config.hasPath(ConfigKey.STORAGE_BACKUP_BAK2PATH) - ? config.getString(ConfigKey.STORAGE_BACKUP_BAK2PATH) : "bak2/database/"; - int frequency = config.hasPath(ConfigKey.STORAGE_BACKUP_FREQUENCY) - ? config.getInt(ConfigKey.STORAGE_BACKUP_FREQUENCY) : 10000; - PARAMETER.dbBackupConfig = DbBackupConfig.getInstance() - .initArgs(enable, propPath, bak1path, bak2path, frequency); - } - - private static void initBackupProperty(Config config) { - PARAMETER.backupPriority = config.hasPath(ConfigKey.NODE_BACKUP_PRIORITY) - ? config.getInt(ConfigKey.NODE_BACKUP_PRIORITY) : 0; - - PARAMETER.backupPort = config.hasPath(ConfigKey.NODE_BACKUP_PORT) - ? config.getInt(ConfigKey.NODE_BACKUP_PORT) : 10001; - - PARAMETER.keepAliveInterval = config.hasPath(ConfigKey.NODE_BACKUP_KEEPALIVEINTERVAL) - ? config.getInt(ConfigKey.NODE_BACKUP_KEEPALIVEINTERVAL) : 3000; - - PARAMETER.backupMembers = config.hasPath(ConfigKey.NODE_BACKUP_MEMBERS) - ? config.getStringList(ConfigKey.NODE_BACKUP_MEMBERS) : new ArrayList<>(); - } + // initRocksDbSettings, initRocksDbBackupProperty, initBackupProperty + // removed — logic moved to applyStorageConfig() and applyNodeBackupConfig() public static void logConfig() { CommonParameter parameter = CommonParameter.getInstance(); diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java deleted file mode 100644 index b21c9c440a4..00000000000 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ /dev/null @@ -1,331 +0,0 @@ -package org.tron.core.config.args; - -/** - * HOCON configuration key constants. - * These map to paths in config files (e.g. config.conf) and are read by Args.setParam(). - */ -final class ConfigKey { - - private ConfigKey() { - } - - // local witness - public static final String LOCAL_WITNESS = "localwitness"; // private key - public static final String LOCAL_WITNESS_ACCOUNT_ADDRESS = "localWitnessAccountAddress"; - public static final String LOCAL_WITNESS_KEYSTORE = "localwitnesskeystore"; - - // crypto - public static final String CRYPTO_ENGINE = "crypto.engine"; - - // vm - public static final String VM_SUPPORT_CONSTANT = "vm.supportConstant"; - public static final String VM_MAX_ENERGY_LIMIT_FOR_CONSTANT = "vm.maxEnergyLimitForConstant"; - public static final String VM_LRU_CACHE_SIZE = "vm.lruCacheSize"; - public static final String VM_MIN_TIME_RATIO = "vm.minTimeRatio"; - public static final String VM_MAX_TIME_RATIO = "vm.maxTimeRatio"; - public static final String VM_LONG_RUNNING_TIME = "vm.longRunningTime"; - public static final String VM_ESTIMATE_ENERGY = "vm.estimateEnergy"; - public static final String VM_ESTIMATE_ENERGY_MAX_RETRY = "vm.estimateEnergyMaxRetry"; - public static final String VM_TRACE = "vm.vmTrace"; - public static final String VM_SAVE_INTERNAL_TX = "vm.saveInternalTx"; - public static final String VM_SAVE_FEATURED_INTERNAL_TX = "vm.saveFeaturedInternalTx"; - public static final String VM_SAVE_CANCEL_ALL_UNFREEZE_V2_DETAILS = - "vm.saveCancelAllUnfreezeV2Details"; - - // genesis - public static final String GENESIS_BLOCK = "genesis.block"; - public static final String GENESIS_BLOCK_TIMESTAMP = "genesis.block.timestamp"; - public static final String GENESIS_BLOCK_PARENTHASH = "genesis.block.parentHash"; - public static final String GENESIS_BLOCK_ASSETS = "genesis.block.assets"; - public static final String GENESIS_BLOCK_WITNESSES = "genesis.block.witnesses"; - - // block - public static final String BLOCK_NEED_SYNC_CHECK = "block.needSyncCheck"; - public static final String BLOCK_MAINTENANCE_TIME_INTERVAL = "block.maintenanceTimeInterval"; - public static final String BLOCK_PROPOSAL_EXPIRE_TIME = "block.proposalExpireTime"; - public static final String BLOCK_CHECK_FROZEN_TIME = "block.checkFrozenTime"; - public static final String BLOCK_CACHE_TIMEOUT = "node.blockCacheTimeout"; - - // node - discovery - public static final String NODE_DISCOVERY_ENABLE = "node.discovery.enable"; - public static final String NODE_DISCOVERY_PERSIST = "node.discovery.persist"; - public static final String NODE_DISCOVERY_EXTERNAL_IP = "node.discovery.external.ip"; - - // node - connection - public static final String NODE_EFFECTIVE_CHECK_ENABLE = "node.effectiveCheckEnable"; - public static final String NODE_CONNECTION_TIMEOUT = "node.connection.timeout"; - public static final String NODE_FETCH_BLOCK_TIMEOUT = "node.fetchBlock.timeout"; - public static final String NODE_CHANNEL_READ_TIMEOUT = "node.channel.read.timeout"; - public static final String NODE_MAX_CONNECTIONS = "node.maxConnections"; - public static final String NODE_MIN_CONNECTIONS = "node.minConnections"; - public static final String NODE_MIN_ACTIVE_CONNECTIONS = "node.minActiveConnections"; - public static final String NODE_MAX_CONNECTIONS_WITH_SAME_IP = "node.maxConnectionsWithSameIp"; - public static final String NODE_MIN_PARTICIPATION_RATE = "node.minParticipationRate"; - public static final String NODE_MAX_ACTIVE_NODES = "node.maxActiveNodes"; - public static final String NODE_MAX_ACTIVE_NODES_WITH_SAME_IP = "node.maxActiveNodesWithSameIp"; - public static final String NODE_CONNECT_FACTOR = "node.connectFactor"; - public static final String NODE_ACTIVE_CONNECT_FACTOR = "node.activeConnectFactor"; - public static final String NODE_IS_OPEN_FULL_TCP_DISCONNECT = "node.isOpenFullTcpDisconnect"; - public static final String NODE_INACTIVE_THRESHOLD = "node.inactiveThreshold"; - public static final String NODE_DETECT_ENABLE = "node.nodeDetectEnable"; - public static final String NODE_MAX_HTTP_CONNECT_NUMBER = "node.maxHttpConnectNumber"; - - // node - p2p - public static final String NODE_LISTEN_PORT = "node.listen.port"; - public static final String NODE_P2P_VERSION = "node.p2p.version"; - public static final String NODE_ENABLE_IPV6 = "node.enableIpv6"; - public static final String NODE_SYNC_FETCH_BATCH_NUM = "node.syncFetchBatchNum"; - public static final String NODE_MAX_TPS = "node.maxTps"; - public static final String NODE_NET_MAX_TRX_PER_SECOND = "node.netMaxTrxPerSecond"; - public static final String NODE_TCP_NETTY_WORK_THREAD_NUM = "node.tcpNettyWorkThreadNum"; - public static final String NODE_UDP_NETTY_WORK_THREAD_NUM = "node.udpNettyWorkThreadNum"; - public static final String NODE_VALIDATE_SIGN_THREAD_NUM = "node.validateSignThreadNum"; - public static final String NODE_RECEIVE_TCP_MIN_DATA_LENGTH = "node.receiveTcpMinDataLength"; - public static final String NODE_PRODUCED_TIMEOUT = "node.blockProducedTimeOut"; - public static final String NODE_MAX_TRANSACTION_PENDING_SIZE = "node.maxTransactionPendingSize"; - public static final String NODE_PENDING_TRANSACTION_TIMEOUT = "node.pendingTransactionTimeout"; - public static final String NODE_ACTIVE = "node.active"; - public static final String NODE_PASSIVE = "node.passive"; - public static final String NODE_FAST_FORWARD = "node.fastForward"; - public static final String NODE_MAX_FAST_FORWARD_NUM = "node.maxFastForwardNum"; - public static final String NODE_AGREE_NODE_COUNT = "node.agreeNodeCount"; - public static final String NODE_SOLIDITY_THREADS = "node.solidity.threads"; - public static final String NODE_TRUST_NODE = "node.trustNode"; - public static final String NODE_WALLET_EXTENSION_API = "node.walletExtensionApi"; - public static final String NODE_VALID_CONTRACT_PROTO_THREADS = "node.validContractProto.threads"; - public static final String NODE_SHIELDED_TRANS_IN_PENDING_MAX_COUNTS = - "node.shieldedTransInPendingMaxCounts"; - public static final String NODE_FULLNODE_ALLOW_SHIELDED_TRANSACTION = - "node.fullNodeAllowShieldedTransaction"; - public static final String ALLOW_SHIELDED_TRANSACTION_API = - "node.allowShieldedTransactionApi"; - public static final String NODE_ZEN_TOKENID = "node.zenTokenId"; - public static final String NODE_OPEN_HISTORY_QUERY_WHEN_LITEFN = - "node.openHistoryQueryWhenLiteFN"; - public static final String NODE_METRICS_ENABLE = "node.metricsEnable"; - public static final String NODE_DISABLED_API_LIST = "node.disabledApi"; - - // node - rpc - public static final String NODE_RPC_PORT = "node.rpc.port"; - public static final String NODE_RPC_SOLIDITY_PORT = "node.rpc.solidityPort"; - public static final String NODE_RPC_PBFT_PORT = "node.rpc.PBFTPort"; - public static final String NODE_RPC_ENABLE = "node.rpc.enable"; - public static final String NODE_RPC_SOLIDITY_ENABLE = "node.rpc.solidityEnable"; - public static final String NODE_RPC_PBFT_ENABLE = "node.rpc.PBFTEnable"; - public static final String NODE_RPC_THREAD = "node.rpc.thread"; - public static final String NODE_RPC_MAX_CONCURRENT_CALLS_PER_CONNECTION = - "node.rpc.maxConcurrentCallsPerConnection"; - public static final String NODE_RPC_FLOW_CONTROL_WINDOW = "node.rpc.flowControlWindow"; - public static final String NODE_RPC_MAX_CONNECTION_IDLE_IN_MILLIS = - "node.rpc.maxConnectionIdleInMillis"; - public static final String NODE_RPC_MAX_RST_STREAM = "node.rpc.maxRstStream"; - public static final String NODE_RPC_SECONDS_PER_WINDOW = "node.rpc.secondsPerWindow"; - public static final String NODE_RPC_MAX_CONNECTION_AGE_IN_MILLIS = - "node.rpc.maxConnectionAgeInMillis"; - public static final String NODE_RPC_MAX_MESSAGE_SIZE = "node.rpc.maxMessageSize"; - public static final String NODE_RPC_MAX_HEADER_LIST_SIZE = "node.rpc.maxHeaderListSize"; - public static final String NODE_RPC_REFLECTION_SERVICE = "node.rpc.reflectionService"; - public static final String NODE_RPC_MIN_EFFECTIVE_CONNECTION = - "node.rpc.minEffectiveConnection"; - public static final String NODE_RPC_TRX_CACHE_ENABLE = "node.rpc.trxCacheEnable"; - - // node - http - public static final String NODE_HTTP_FULLNODE_PORT = "node.http.fullNodePort"; - public static final String NODE_HTTP_SOLIDITY_PORT = "node.http.solidityPort"; - public static final String NODE_HTTP_FULLNODE_ENABLE = "node.http.fullNodeEnable"; - public static final String NODE_HTTP_SOLIDITY_ENABLE = "node.http.solidityEnable"; - public static final String NODE_HTTP_PBFT_ENABLE = "node.http.PBFTEnable"; - public static final String NODE_HTTP_PBFT_PORT = "node.http.PBFTPort"; - - // node - jsonrpc - public static final String NODE_JSONRPC_HTTP_FULLNODE_ENABLE = - "node.jsonrpc.httpFullNodeEnable"; - public static final String NODE_JSONRPC_HTTP_FULLNODE_PORT = "node.jsonrpc.httpFullNodePort"; - public static final String NODE_JSONRPC_HTTP_SOLIDITY_ENABLE = - "node.jsonrpc.httpSolidityEnable"; - public static final String NODE_JSONRPC_HTTP_SOLIDITY_PORT = "node.jsonrpc.httpSolidityPort"; - public static final String NODE_JSONRPC_HTTP_PBFT_ENABLE = "node.jsonrpc.httpPBFTEnable"; - public static final String NODE_JSONRPC_HTTP_PBFT_PORT = "node.jsonrpc.httpPBFTPort"; - public static final String NODE_JSONRPC_MAX_BLOCK_RANGE = "node.jsonrpc.maxBlockRange"; - public static final String NODE_JSONRPC_MAX_SUB_TOPICS = "node.jsonrpc.maxSubTopics"; - public static final String NODE_JSONRPC_MAX_BLOCK_FILTER_NUM = - "node.jsonrpc.maxBlockFilterNum"; - - // node - dns - public static final String NODE_DNS_TREE_URLS = "node.dns.treeUrls"; - public static final String NODE_DNS_PUBLISH = "node.dns.publish"; - public static final String NODE_DNS_DOMAIN = "node.dns.dnsDomain"; - public static final String NODE_DNS_CHANGE_THRESHOLD = "node.dns.changeThreshold"; - public static final String NODE_DNS_MAX_MERGE_SIZE = "node.dns.maxMergeSize"; - public static final String NODE_DNS_PRIVATE = "node.dns.dnsPrivate"; - public static final String NODE_DNS_KNOWN_URLS = "node.dns.knownUrls"; - public static final String NODE_DNS_STATIC_NODES = "node.dns.staticNodes"; - public static final String NODE_DNS_SERVER_TYPE = "node.dns.serverType"; - public static final String NODE_DNS_ACCESS_KEY_ID = "node.dns.accessKeyId"; - public static final String NODE_DNS_ACCESS_KEY_SECRET = "node.dns.accessKeySecret"; - public static final String NODE_DNS_ALIYUN_ENDPOINT = "node.dns.aliyunDnsEndpoint"; - public static final String NODE_DNS_AWS_REGION = "node.dns.awsRegion"; - public static final String NODE_DNS_AWS_HOST_ZONE_ID = "node.dns.awsHostZoneId"; - - // node - backup - public static final String NODE_BACKUP_PRIORITY = "node.backup.priority"; - public static final String NODE_BACKUP_PORT = "node.backup.port"; - public static final String NODE_BACKUP_KEEPALIVEINTERVAL = "node.backup.keepAliveInterval"; - public static final String NODE_BACKUP_MEMBERS = "node.backup.members"; - - // node - shutdown - public static final String NODE_SHUTDOWN_BLOCK_TIME = "node.shutdown.BlockTime"; - public static final String NODE_SHUTDOWN_BLOCK_HEIGHT = "node.shutdown.BlockHeight"; - public static final String NODE_SHUTDOWN_BLOCK_COUNT = "node.shutdown.BlockCount"; - - // node - dynamic config - public static final String DYNAMIC_CONFIG_ENABLE = "node.dynamicConfig.enable"; - public static final String DYNAMIC_CONFIG_CHECK_INTERVAL = "node.dynamicConfig.checkInterval"; - - // node - unsolidified - public static final String UNSOLIDIFIED_BLOCK_CHECK = "node.unsolidifiedBlockCheck"; - public static final String MAX_UNSOLIDIFIED_BLOCKS = "node.maxUnsolidifiedBlocks"; - - // node - misc - public static final String OPEN_PRINT_LOG = "node.openPrintLog"; - public static final String OPEN_TRANSACTION_SORT = "node.openTransactionSort"; - - // committee - public static final String COMMITTEE_ALLOW_CREATION_OF_CONTRACTS = - "committee.allowCreationOfContracts"; - public static final String COMMITTEE_ALLOW_MULTI_SIGN = "committee.allowMultiSign"; - public static final String COMMITTEE_ALLOW_ADAPTIVE_ENERGY = "committee.allowAdaptiveEnergy"; - public static final String COMMITTEE_ALLOW_DELEGATE_RESOURCE = - "committee.allowDelegateResource"; - public static final String COMMITTEE_ALLOW_SAME_TOKEN_NAME = "committee.allowSameTokenName"; - public static final String COMMITTEE_ALLOW_TVM_TRANSFER_TRC10 = - "committee.allowTvmTransferTrc10"; - public static final String COMMITTEE_ALLOW_TVM_CONSTANTINOPLE = - "committee.allowTvmConstantinople"; - public static final String COMMITTEE_ALLOW_TVM_SOLIDITY059 = "committee.allowTvmSolidity059"; - public static final String COMMITTEE_FORBID_TRANSFER_TO_CONTRACT = - "committee.forbidTransferToContract"; - public static final String COMMITTEE_ALLOW_SHIELDED_TRC20_TRANSACTION = - "committee.allowShieldedTRC20Transaction"; - public static final String COMMITTEE_ALLOW_TVM_ISTANBUL = "committee.allowTvmIstanbul"; - public static final String COMMITTEE_ALLOW_MARKET_TRANSACTION = - "committee.allowMarketTransaction"; - public static final String COMMITTEE_ALLOW_PROTO_FILTER_NUM = - "committee.allowProtoFilterNum"; - public static final String COMMITTEE_ALLOW_ACCOUNT_STATE_ROOT = - "committee.allowAccountStateRoot"; - public static final String COMMITTEE_ALLOW_PBFT = "committee.allowPBFT"; - public static final String COMMITTEE_PBFT_EXPIRE_NUM = "committee.pBFTExpireNum"; - public static final String COMMITTEE_ALLOW_TRANSACTION_FEE_POOL = - "committee.allowTransactionFeePool"; - public static final String COMMITTEE_ALLOW_BLACK_HOLE_OPTIMIZATION = - "committee.allowBlackHoleOptimization"; - public static final String COMMITTEE_ALLOW_NEW_RESOURCE_MODEL = - "committee.allowNewResourceModel"; - public static final String COMMITTEE_ALLOW_RECEIPTS_MERKLE_ROOT = - "committee.allowReceiptsMerkleRoot"; - public static final String COMMITTEE_ALLOW_TVM_FREEZE = "committee.allowTvmFreeze"; - public static final String COMMITTEE_ALLOW_TVM_VOTE = "committee.allowTvmVote"; - public static final String COMMITTEE_UNFREEZE_DELAY_DAYS = "committee.unfreezeDelayDays"; - public static final String COMMITTEE_ALLOW_TVM_LONDON = "committee.allowTvmLondon"; - public static final String COMMITTEE_ALLOW_TVM_COMPATIBLE_EVM = - "committee.allowTvmCompatibleEvm"; - public static final String COMMITTEE_ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX = - "committee.allowHigherLimitForMaxCpuTimeOfOneTx"; - public static final String COMMITTEE_ALLOW_NEW_REWARD_ALGORITHM = - "committee.allowNewRewardAlgorithm"; - public static final String COMMITTEE_ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID = - "committee.allowOptimizedReturnValueOfChainId"; - public static final String COMMITTEE_CHANGED_DELEGATION = "committee.changedDelegation"; - public static final String COMMITTEE_ALLOW_TVM_SHANGHAI = "committee.allowTvmShangHai"; - public static final String COMMITTEE_ALLOW_OLD_REWARD_OPT = "committee.allowOldRewardOpt"; - public static final String COMMITTEE_ALLOW_ENERGY_ADJUSTMENT = - "committee.allowEnergyAdjustment"; - public static final String COMMITTEE_ALLOW_STRICT_MATH = "committee.allowStrictMath"; - public static final String COMMITTEE_CONSENSUS_LOGIC_OPTIMIZATION = - "committee.consensusLogicOptimization"; - public static final String COMMITTEE_ALLOW_TVM_CANCUN = "committee.allowTvmCancun"; - public static final String COMMITTEE_ALLOW_TVM_BLOB = "committee.allowTvmBlob"; - public static final String COMMITTEE_PROPOSAL_EXPIRE_TIME = "committee.proposalExpireTime"; - public static final String COMMITTEE_ALLOW_TVM_OSAKA = "committee.allowTvmOsaka"; - public static final String ALLOW_ACCOUNT_ASSET_OPTIMIZATION = - "committee.allowAccountAssetOptimization"; - public static final String ALLOW_ASSET_OPTIMIZATION = "committee.allowAssetOptimization"; - public static final String ALLOW_NEW_REWARD = "committee.allowNewReward"; - public static final String MEMO_FEE = "committee.memoFee"; - public static final String ALLOW_DELEGATE_OPTIMIZATION = - "committee.allowDelegateOptimization"; - public static final String ALLOW_DYNAMIC_ENERGY = "committee.allowDynamicEnergy"; - public static final String DYNAMIC_ENERGY_THRESHOLD = "committee.dynamicEnergyThreshold"; - public static final String DYNAMIC_ENERGY_INCREASE_FACTOR = - "committee.dynamicEnergyIncreaseFactor"; - public static final String DYNAMIC_ENERGY_MAX_FACTOR = "committee.dynamicEnergyMaxFactor"; - - // storage - public static final String STORAGE_NEEDTO_UPDATE_ASSET = "storage.needToUpdateAsset"; - public static final String STORAGE_BACKUP_ENABLE = "storage.backup.enable"; - public static final String STORAGE_BACKUP_PROP_PATH = "storage.backup.propPath"; - public static final String STORAGE_BACKUP_BAK1PATH = "storage.backup.bak1path"; - public static final String STORAGE_BACKUP_BAK2PATH = "storage.backup.bak2path"; - public static final String STORAGE_BACKUP_FREQUENCY = "storage.backup.frequency"; - public static final String STORAGE_DB_SETTING = "storage.dbSettings."; - public static final String HISTORY_BALANCE_LOOKUP = "storage.balance.history.lookup"; - - // event - public static final String EVENT_SUBSCRIBE = "event.subscribe"; - public static final String EVENT_SUBSCRIBE_ENABLE = "event.subscribe.enable"; - public static final String EVENT_SUBSCRIBE_FILTER = "event.subscribe.filter"; - public static final String EVENT_SUBSCRIBE_VERSION = "event.subscribe.version"; - public static final String EVENT_SUBSCRIBE_START_SYNC_BLOCK_NUM = - "event.subscribe.startSyncBlockNum"; - public static final String EVENT_SUBSCRIBE_PATH = "event.subscribe.path"; - public static final String EVENT_SUBSCRIBE_SERVER = "event.subscribe.server"; - public static final String EVENT_SUBSCRIBE_DB_CONFIG = "event.subscribe.dbconfig"; - public static final String EVENT_SUBSCRIBE_TOPICS = "event.subscribe.topics"; - public static final String EVENT_SUBSCRIBE_FROM_BLOCK = "event.subscribe.filter.fromblock"; - public static final String EVENT_SUBSCRIBE_TO_BLOCK = "event.subscribe.filter.toblock"; - public static final String EVENT_SUBSCRIBE_CONTRACT_ADDRESS = - "event.subscribe.filter.contractAddress"; - public static final String EVENT_SUBSCRIBE_CONTRACT_TOPIC = - "event.subscribe.filter.contractTopic"; - public static final String USE_NATIVE_QUEUE = "event.subscribe.native.useNativeQueue"; - public static final String NATIVE_QUEUE_BIND_PORT = "event.subscribe.native.bindport"; - public static final String NATIVE_QUEUE_SEND_LENGTH = - "event.subscribe.native.sendqueuelength"; - - // rate limiter - public static final String RATE_LIMITER = "rate.limiter"; - public static final String RATE_LIMITER_GLOBAL_QPS = "rate.limiter.global.qps"; - public static final String RATE_LIMITER_GLOBAL_IP_QPS = "rate.limiter.global.ip.qps"; - public static final String RATE_LIMITER_GLOBAL_API_QPS = "rate.limiter.global.api.qps"; - public static final String RATE_LIMITER_HTTP = "rate.limiter.http"; - public static final String RATE_LIMITER_RPC = "rate.limiter.rpc"; - public static final String RATE_LIMITER_P2P_SYNC_BLOCK_CHAIN = - "rate.limiter.p2p.syncBlockChain"; - public static final String RATE_LIMITER_P2P_FETCH_INV_DATA = "rate.limiter.p2p.fetchInvData"; - public static final String RATE_LIMITER_P2P_DISCONNECT = "rate.limiter.p2p.disconnect"; - - // metrics - public static final String METRICS_STORAGE_ENABLE = "node.metrics.storageEnable"; - public static final String METRICS_INFLUXDB_IP = "node.metrics.influxdb.ip"; - public static final String METRICS_INFLUXDB_PORT = "node.metrics.influxdb.port"; - public static final String METRICS_INFLUXDB_DATABASE = "node.metrics.influxdb.database"; - public static final String METRICS_REPORT_INTERVAL = - "node.metrics.influxdb.metricsReportInterval"; - public static final String METRICS_PROMETHEUS_ENABLE = "node.metrics.prometheus.enable"; - public static final String METRICS_PROMETHEUS_PORT = "node.metrics.prometheus.port"; - - // seed - public static final String SEED_NODE_IP_LIST = "seed.node.ip.list"; - - // transaction - public static final String TRX_REFERENCE_BLOCK = "trx.reference.block"; - public static final String TRX_EXPIRATION_TIME_IN_MILLIS_SECONDS = - "trx.expiration.timeInMilliseconds"; - - // energy - public static final String ENERGY_LIMIT_BLOCK_NUM = "enery.limit.block.num"; - - // actuator - public static final String ACTUATOR_WHITELIST = "actuator.whitelist"; -} diff --git a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java index 04ba1b306eb..5a9923b16c9 100644 --- a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java +++ b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java @@ -61,15 +61,16 @@ public void run() { public void reload() { logger.debug("Reloading ... "); Config config = Configuration.getByFileName(Args.getConfigFilePath()); + NodeConfig nodeConfig = NodeConfig.fromConfig(config); - updateActiveNodes(config); + updateActiveNodes(nodeConfig); - updateTrustNodes(config); + updateTrustNodes(nodeConfig); } - private void updateActiveNodes(Config config) { + private void updateActiveNodes(NodeConfig nodeConfig) { List newActiveNodes = - Args.getInetSocketAddress(config, ConfigKey.NODE_ACTIVE, true); + Args.filterInetSocketAddress(nodeConfig.getActive(), true); parameter.setActiveNodes(newActiveNodes); List activeNodes = TronNetService.getP2pConfig().getActiveNodes(); activeNodes.clear(); @@ -78,8 +79,11 @@ private void updateActiveNodes(Config config) { TronNetService.getP2pConfig().getActiveNodes().toString()); } - private void updateTrustNodes(Config config) { - List newPassiveNodes = Args.getInetAddress(config, ConfigKey.NODE_PASSIVE); + private void updateTrustNodes(NodeConfig nodeConfig) { + List newPassiveNodes = new java.util.ArrayList<>(); + for (InetSocketAddress sa : Args.filterInetSocketAddress(nodeConfig.getPassive(), false)) { + newPassiveNodes.add(sa.getAddress()); + } parameter.setPassiveNodes(newPassiveNodes); List trustNodes = TronNetService.getP2pConfig().getTrustNodes(); trustNodes.clear(); diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java index 30711eb6190..c2ce2ba0046 100644 --- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java +++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java @@ -79,12 +79,27 @@ public static LocalWitnesses initFromKeystore( List privateKeys = new ArrayList<>(); try { - Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName)); + Credentials credentials = WalletUtils.loadCredentials(pwd, new File(fileName), + Args.getInstance().isECKeyCryptoEngine()); SignInterface sign = credentials.getSignInterface(); String prikey = ByteArray.toHexString(sign.getPrivateKey()); privateKeys.add(prikey); } catch (IOException | CipherException e) { logger.error("Witness node start failed!"); + // Legacy-truncation hint: if this keystore was created with + // `FullNode.jar --keystore-factory` in non-TTY mode (e.g. + // `echo PASS | java ...`), the legacy code encrypted with only + // the first whitespace-separated word of the password. Emit the + // tip only when the entered password has internal whitespace — + // otherwise truncation cannot be the cause. + if (e instanceof CipherException && pwd != null && pwd.matches(".*\\s.*")) { + logger.error( + "Tip: keystores created via `FullNode.jar --keystore-factory` in " + + "non-TTY mode were encrypted with only the first " + + "whitespace-separated word of the password. Try restarting " + + "with only that first word as `-p`, then reset the password " + + "via `java -jar Toolkit.jar keystore update`."); + } throw new TronError(e, TronError.ErrCode.WITNESS_KEYSTORE_LOAD); } diff --git a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java index 3ad4ace62fc..5a3b86cb396 100644 --- a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java @@ -297,6 +297,7 @@ public FullNodeHttpApiService() { port = Args.getInstance().getFullNodeHttpPort(); enable = isFullNode() && Args.getInstance().isFullNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..1fefdde5e91 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -327,10 +327,12 @@ public static Transaction packTransaction(String strTransaction, boolean selfTyp } } + @Deprecated public static void checkBodySize(String body) throws Exception { CommonParameter parameter = Args.getInstance(); - if (body.getBytes().length > parameter.getMaxMessageSize()) { - throw new Exception("body size is too big, the limit is " + parameter.getMaxMessageSize()); + if (body.getBytes().length > parameter.getHttpMaxMessageSize()) { + throw new Exception("body size is too big, the limit is " + + parameter.getHttpMaxMessageSize()); } } diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java index 359adfc2b39..0c4843c0550 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/SolidityNodeHttpApiService.java @@ -170,6 +170,7 @@ public SolidityNodeHttpApiService() { port = Args.getInstance().getSolidityHttpPort(); enable = !isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java index fffaf8d4e7b..5282ef5c819 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnPBFT/JsonRpcServiceOnPBFT.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnPBFT() { port = Args.getInstance().getJsonRpcHttpPBFTPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpPBFTNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java index a6f7d5dd5e7..8b52066d5f8 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceJsonRpcOnSolidity/JsonRpcServiceOnSolidity.java @@ -19,6 +19,7 @@ public JsonRpcServiceOnSolidity() { port = Args.getInstance().getJsonRpcHttpSolidityPort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpSolidityNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java index a77b45353c9..c0616c2ae78 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/http/PBFT/HttpApiOnPBFTService.java @@ -173,6 +173,7 @@ public HttpApiOnPBFTService() { port = Args.getInstance().getPBFTHttpPort(); enable = isFullNode() && Args.getInstance().isPBFTHttpEnable(); contextPath = "/walletpbft"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java index f69597959f8..33e325bd578 100644 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/http/solidity/HttpApiOnSolidityService.java @@ -181,6 +181,7 @@ public HttpApiOnSolidityService() { port = Args.getInstance().getSolidityHttpPort(); enable = isFullNode() && Args.getInstance().isSolidityNodeHttpEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getHttpMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java index 566ad33a722..ffe81bfa100 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/FullNodeJsonRpcHttpService.java @@ -24,6 +24,7 @@ public FullNodeJsonRpcHttpService() { port = Args.getInstance().getJsonRpcHttpFullNodePort(); enable = isFullNode() && Args.getInstance().isJsonRpcHttpFullNodeEnable(); contextPath = "/"; + maxRequestSize = Args.getInstance().getJsonRpcMaxMessageSize(); } @Override diff --git a/framework/src/main/java/org/tron/core/trie/TrieImpl.java b/framework/src/main/java/org/tron/core/trie/TrieImpl.java index b256cbe323d..586c3b2b893 100644 --- a/framework/src/main/java/org/tron/core/trie/TrieImpl.java +++ b/framework/src/main/java/org/tron/core/trie/TrieImpl.java @@ -179,14 +179,18 @@ private Node insert(Node n, TrieKey k, Object nodeOrValue) { } else { TrieKey currentNodeKey = n.kvNodeGetKey(); TrieKey commonPrefix = k.getCommonPrefix(currentNodeKey); - if (commonPrefix.isEmpty()) { + // NOTE: equals(k) MUST precede isEmpty(). They overlap only when both k and + // currentNodeKey are empty (duplicate put on a fully-split KV leaf); in that + // case the correct behavior is an in-place value update, not conversion to + // a BranchNode. Swapping the order corrupts the trie structure. See #6608. + if (commonPrefix.equals(k)) { + return n.kvNodeSetValueOrNode(nodeOrValue); + } else if (commonPrefix.isEmpty()) { Node newBranchNode = new Node(); insert(newBranchNode, currentNodeKey, n.kvNodeGetValueOrNode()); insert(newBranchNode, k, nodeOrValue); n.dispose(); return newBranchNode; - } else if (commonPrefix.equals(k)) { - return n.kvNodeSetValueOrNode(nodeOrValue); } else if (commonPrefix.equals(currentNodeKey)) { insert(n.kvNodeGetChildNode(), k.shift(commonPrefix.getLength()), nodeOrValue); return n.invalidate(); @@ -873,6 +877,11 @@ public Object kvNodeGetValueOrNode() { public Node kvNodeSetValueOrNode(Object valueOrNode) { parse(); assert getType() != NodeType.BranchNode; + if (valueOrNode instanceof byte[] && children[1] instanceof byte[] + && (children[1] == valueOrNode + || Arrays.equals((byte[]) children[1], (byte[]) valueOrNode))) { + return this; + } children[1] = valueOrNode; dirty = true; return this; diff --git a/framework/src/main/java/org/tron/keystore/WalletUtils.java b/framework/src/main/java/org/tron/keystore/WalletUtils.java deleted file mode 100644 index 8bcc68cbab0..00000000000 --- a/framework/src/main/java/org/tron/keystore/WalletUtils.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.tron.keystore; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.Console; -import java.io.File; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Scanner; -import org.apache.commons.lang3.StringUtils; -import org.tron.common.crypto.SignInterface; -import org.tron.common.crypto.SignUtils; -import org.tron.common.utils.Utils; -import org.tron.core.config.args.Args; -import org.tron.core.exception.CipherException; - -/** - * Utility functions for working with Wallet files. - */ -public class WalletUtils { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - static { - objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - public static String generateFullNewWalletFile(String password, File destinationDirectory) - throws NoSuchAlgorithmException, NoSuchProviderException, - InvalidAlgorithmParameterException, CipherException, IOException { - - return generateNewWalletFile(password, destinationDirectory, true); - } - - public static String generateLightNewWalletFile(String password, File destinationDirectory) - throws NoSuchAlgorithmException, NoSuchProviderException, - InvalidAlgorithmParameterException, CipherException, IOException { - - return generateNewWalletFile(password, destinationDirectory, false); - } - - public static String generateNewWalletFile( - String password, File destinationDirectory, boolean useFullScrypt) - throws CipherException, IOException, InvalidAlgorithmParameterException, - NoSuchAlgorithmException, NoSuchProviderException { - - SignInterface ecKeyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), - Args.getInstance().isECKeyCryptoEngine()); - return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt); - } - - public static String generateWalletFile( - String password, SignInterface ecKeyPair, File destinationDirectory, boolean useFullScrypt) - throws CipherException, IOException { - - WalletFile walletFile; - if (useFullScrypt) { - walletFile = Wallet.createStandard(password, ecKeyPair); - } else { - walletFile = Wallet.createLight(password, ecKeyPair); - } - - String fileName = getWalletFileName(walletFile); - File destination = new File(destinationDirectory, fileName); - - objectMapper.writeValue(destination, walletFile); - - return fileName; - } - - public static Credentials loadCredentials(String password, File source) - throws IOException, CipherException { - WalletFile walletFile = objectMapper.readValue(source, WalletFile.class); - return Credentials.create(Wallet.decrypt(password, walletFile)); - } - - private static String getWalletFileName(WalletFile walletFile) { - DateTimeFormatter format = DateTimeFormatter.ofPattern( - "'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'"); - ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); - - return now.format(format) + walletFile.getAddress() + ".json"; - } - - public static String getDefaultKeyDirectory() { - return getDefaultKeyDirectory(System.getProperty("os.name")); - } - - static String getDefaultKeyDirectory(String osName1) { - String osName = osName1.toLowerCase(); - - if (osName.startsWith("mac")) { - return String.format( - "%s%sLibrary%sEthereum", System.getProperty("user.home"), File.separator, - File.separator); - } else if (osName.startsWith("win")) { - return String.format("%s%sEthereum", System.getenv("APPDATA"), File.separator); - } else { - return String.format("%s%s.ethereum", System.getProperty("user.home"), File.separator); - } - } - - public static String getTestnetKeyDirectory() { - return String.format( - "%s%stestnet%skeystore", getDefaultKeyDirectory(), File.separator, File.separator); - } - - public static String getMainnetKeyDirectory() { - return String.format("%s%skeystore", getDefaultKeyDirectory(), File.separator); - } - - public static boolean passwordValid(String password) { - if (StringUtils.isEmpty(password)) { - return false; - } - if (password.length() < 6) { - return false; - } - //Other rule; - return true; - } - - public static String inputPassword() { - Scanner in = null; - String password; - Console cons = System.console(); - if (cons == null) { - in = new Scanner(System.in); - } - while (true) { - if (cons != null) { - char[] pwd = cons.readPassword("password: "); - password = String.valueOf(pwd); - } else { - String input = in.nextLine().trim(); - password = input.split("\\s+")[0]; - } - if (passwordValid(password)) { - return password; - } - System.out.println("Invalid password, please input again."); - } - } - - public static String inputPassword2Twice() { - String password0; - while (true) { - System.out.println("Please input password."); - password0 = inputPassword(); - System.out.println("Please input password again."); - String password1 = inputPassword(); - if (password0.equals(password1)) { - break; - } - System.out.println("Two passwords do not match, please input again."); - } - return password0; - } -} diff --git a/framework/src/main/java/org/tron/program/KeystoreFactory.java b/framework/src/main/java/org/tron/program/KeystoreFactory.java index 8199d7e9076..a88cdca904a 100755 --- a/framework/src/main/java/org/tron/program/KeystoreFactory.java +++ b/framework/src/main/java/org/tron/program/KeystoreFactory.java @@ -15,11 +15,20 @@ import org.tron.keystore.WalletUtils; @Slf4j(topic = "app") +@Deprecated public class KeystoreFactory { private static final String FilePath = "Wallet"; public static void start() { + System.err.println("WARNING: --keystore-factory is deprecated and will be removed " + + "in a future release."); + System.err.println("Please use: java -jar Toolkit.jar keystore "); + System.err.println(" keystore new - Generate a new keystore"); + System.err.println(" keystore import - Import a private key"); + System.err.println(" keystore list - List keystores"); + System.err.println(" keystore update - Change password"); + System.err.println(); KeystoreFactory cli = new KeystoreFactory(); cli.run(); } @@ -57,15 +66,16 @@ private void fileCheck(File file) throws IOException { private void genKeystore() throws CipherException, IOException { + boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine(); String password = WalletUtils.inputPassword2Twice(); - SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random, - CommonParameter.getInstance().isECKeyCryptoEngine()); + SignInterface eCkey = SignUtils.getGeneratedRandomSign(Utils.random, ecKey); File file = new File(FilePath); fileCheck(file); String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true); System.out.println("Gen a keystore its name " + fileName); - Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName)); + Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName), + ecKey); System.out.println("Your address is " + credentials.getAddress()); } @@ -84,22 +94,25 @@ private void importPrivateKey() throws CipherException, IOException { String password = WalletUtils.inputPassword2Twice(); - SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey), - CommonParameter.getInstance().isECKeyCryptoEngine()); + boolean ecKey = CommonParameter.getInstance().isECKeyCryptoEngine(); + SignInterface eCkey = SignUtils.fromPrivate(ByteArray.fromHexString(privateKey), ecKey); File file = new File(FilePath); fileCheck(file); String fileName = WalletUtils.generateWalletFile(password, eCkey, file, true); System.out.println("Gen a keystore its name " + fileName); - Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName)); + Credentials credentials = WalletUtils.loadCredentials(password, new File(file, fileName), + ecKey); System.out.println("Your address is " + credentials.getAddress()); } private void help() { - System.out.println("You can enter the following command: "); - System.out.println("GenKeystore"); - System.out.println("ImportPrivateKey"); - System.out.println("Exit or Quit"); - System.out.println("Input any one of them, you will get more tips."); + System.out.println("NOTE: --keystore-factory is deprecated. Use Toolkit.jar instead:"); + System.out.println(" java -jar Toolkit.jar keystore new|import|list|update"); + System.out.println(); + System.out.println("Legacy commands (will be removed):"); + System.out.println(" GenKeystore"); + System.out.println(" ImportPrivateKey"); + System.out.println(" Exit or Quit"); } private void run() { diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java index 3367141e2a5..6ffa3b3ce92 100644 --- a/framework/src/main/java/org/tron/program/SolidityNode.java +++ b/framework/src/main/java/org/tron/program/SolidityNode.java @@ -2,7 +2,10 @@ import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; +import com.google.common.annotations.VisibleForTesting; +import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -11,6 +14,7 @@ import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.client.DatabaseGrpcClient; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.Metrics; import org.tron.core.ChainBaseManager; @@ -39,6 +43,9 @@ public class SolidityNode { private volatile boolean flag = true; + private ExecutorService getBlockEs; + private ExecutorService processBlockEs; + public SolidityNode(Manager dbManager) { this.dbManager = dbManager; this.chainBaseManager = dbManager.getChainBaseManager(); @@ -72,13 +79,26 @@ public static void start() { appT.startup(); SolidityNode node = new SolidityNode(appT.getDbManager()); node.run(); - appT.blockUntilShutdown(); + awaitShutdown(appT, node); + } + + @VisibleForTesting + static void awaitShutdown(Application appT, SolidityNode node) { + try { + appT.blockUntilShutdown(); + } finally { + // SolidityNode is created manually rather than managed by Spring/Application, + // so its executors must be shut down explicitly on exit. + node.shutdown(); + } } private void run() { try { - new Thread(this::getBlock).start(); - new Thread(this::processBlock).start(); + getBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-get-block"); + processBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-process-block"); + getBlockEs.execute(this::getBlock); + processBlockEs.execute(this::processBlock); logger.info("Success to start solid node, ID: {}, remoteBlockNum: {}.", ID.get(), remoteBlockNum); } catch (Exception e) { @@ -88,6 +108,15 @@ private void run() { } } + public void shutdown() { + flag = false; + // Signal both pools before awaiting either so they drain concurrently + getBlockEs.shutdown(); + processBlockEs.shutdown(); + ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "solid-get-block"); + ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "solid-process-block"); + } + private void getBlock() { long blockNum = ID.incrementAndGet(); while (flag) { @@ -137,7 +166,7 @@ private void loopProcessBlock(Block block) { } private Block getBlockByNum(long blockNum) { - while (true) { + while (flag) { try { long time = System.currentTimeMillis(); Block block = databaseGrpcClient.getBlock(blockNum); @@ -155,10 +184,11 @@ private Block getBlockByNum(long blockNum) { sleep(exceptionSleepTime); } } + return null; } private long getLastSolidityBlockNum() { - while (true) { + while (flag) { try { long time = System.currentTimeMillis(); long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum(); @@ -171,6 +201,7 @@ private long getLastSolidityBlockNum() { sleep(exceptionSleepTime); } } + return 0; } public void sleep(long time) { @@ -193,4 +224,4 @@ private void resolveCompatibilityIssueIfUsingFullNodeDatabase() { chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum); } } -} \ No newline at end of file +} diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..e5ea11dd90e 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -223,6 +223,12 @@ 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 { @@ -248,8 +254,11 @@ node { # 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 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 = @@ -357,6 +366,12 @@ node { # 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 diff --git a/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java b/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java new file mode 100644 index 00000000000..1d26f895b64 --- /dev/null +++ b/framework/src/test/java/org/tron/common/ClassLevelAppContextFixture.java @@ -0,0 +1,62 @@ +package org.tron.common; + +import io.grpc.ManagedChannel; +import java.util.concurrent.TimeUnit; +import org.tron.common.application.ApplicationFactory; +import org.tron.common.application.TronApplicationContext; +import org.tron.core.config.DefaultConfig; + +/** + * Shared class-level fixture for tests that manually manage a TronApplicationContext. + */ +public class ClassLevelAppContextFixture { + + private TronApplicationContext context; + + public TronApplicationContext createContext() { + context = new TronApplicationContext(DefaultConfig.class); + return context; + } + + public TronApplicationContext createAndStart() { + createContext(); + startApp(); + return context; + } + + public void startApp() { + ApplicationFactory.create(context).startup(); + } + + public TronApplicationContext getContext() { + return context; + } + + public void close() { + if (context != null) { + context.close(); + context = null; + } + } + + public static void shutdownChannel(ManagedChannel channel) { + if (channel == null) { + return; + } + try { + channel.shutdown(); + if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { + channel.shutdownNow(); + } + } catch (InterruptedException e) { + channel.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public static void shutdownChannels(ManagedChannel... channels) { + for (ManagedChannel channel : channels) { + shutdownChannel(channel); + } + } +} diff --git a/framework/src/test/java/org/tron/common/backup/BackupServerTest.java b/framework/src/test/java/org/tron/common/backup/BackupServerTest.java index ae5f74d8b71..50778970d87 100644 --- a/framework/src/test/java/org/tron/common/backup/BackupServerTest.java +++ b/framework/src/test/java/org/tron/common/backup/BackupServerTest.java @@ -46,7 +46,7 @@ public void tearDown() { @Test(timeout = 60_000) public void test() throws InterruptedException { backupServer.initServer(); - // wait for the server to start + // wait for the server to start so channel is assigned before close() is called Thread.sleep(1000); } } diff --git a/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java new file mode 100644 index 00000000000..685a861bc92 --- /dev/null +++ b/framework/src/test/java/org/tron/common/jetty/SizeLimitHandlerTest.java @@ -0,0 +1,327 @@ +package org.tron.common.jetty; + +import com.alibaba.fastjson.JSONObject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; +import org.tron.common.utils.PublicMethod; +import org.tron.core.config.args.Args; + +/** + * Tests {@link org.eclipse.jetty.server.handler.SizeLimitHandler} body-size + * enforcement configured in {@link HttpService#initContextHandler()}. + * + * Covers: accept/reject by size, UTF-8 byte counting, independent limits + * across HttpService instances, chunked transfer, and zero-limit behavior. + * + * Real JsonRpcServlet integration is tested separately in + * {@code JsonrpcServiceTest#testJsonRpcSizeLimitIntegration}. + */ +@Slf4j +public class SizeLimitHandlerTest { + + private static final int HTTP_MAX_BODY_SIZE = 1024; + private static final int SECOND_SERVICE_MAX_BODY_SIZE = 512; + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private static TestHttpService httpService; + private static SecondHttpService secondService; + private static URI httpServerUri; + private static URI secondServerUri; + private static CloseableHttpClient client; + + /** + * Simulates the real servlet pattern: reads body via getReader(), wraps in + * broad catch(Exception) - mirrors what RateLimiterServlet + actual servlets do. + */ + public static class BroadCatchServlet extends HttpServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + String body = req.getReader().lines() + .collect(Collectors.joining(System.lineSeparator())); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"size\":" + body.length() + + ",\"bytes\":" + body.getBytes().length + "}"); + } catch (Exception e) { + // Mimics RateLimiterServlet line 119-120: silently logs, does not rethrow + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().println("{\"Error\":\"" + e.getClass().getSimpleName() + "\"}"); + } + } + } + + /** Minimal concrete {@link HttpService} wired with a given size limit. */ + static class TestHttpService extends HttpService { + TestHttpService(int port, long maxRequestSize) { + this.port = port; + this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); + } + } + + /** Second HttpService instance with a different size limit, for independence tests. */ + static class SecondHttpService extends HttpService { + SecondHttpService(int port, long maxRequestSize) { + this.port = port; + this.contextPath = "/"; + this.maxRequestSize = maxRequestSize; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(new BroadCatchServlet()), "/*"); + } + } + + @BeforeClass + public static void setup() throws Exception { + Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + Args.getInstance().setHttpMaxMessageSize(HTTP_MAX_BODY_SIZE); + Args.getInstance().setJsonRpcMaxMessageSize(SECOND_SERVICE_MAX_BODY_SIZE); + + int httpPort = PublicMethod.chooseRandomPort(); + httpService = new TestHttpService(httpPort, HTTP_MAX_BODY_SIZE); + httpService.start().get(10, TimeUnit.SECONDS); + httpServerUri = new URI(String.format("http://localhost:%d/", httpPort)); + + int secondPort = PublicMethod.chooseRandomPort(); + secondService = new SecondHttpService(secondPort, SECOND_SERVICE_MAX_BODY_SIZE); + secondService.start().get(10, TimeUnit.SECONDS); + secondServerUri = new URI(String.format("http://localhost:%d/", secondPort)); + + client = HttpClients.createDefault(); + } + + @AfterClass + public static void teardown() throws Exception { + try { + if (client != null) { + client.close(); + } + } finally { + try { + if (httpService != null) { + httpService.stop(); + } + } finally { + if (secondService != null) { + secondService.stop(); + } + } + Args.clearParam(); + } + } + + @Test + public void testHttpBodyWithinLimit() throws Exception { + Assert.assertEquals(200, post(httpServerUri, new StringEntity("small body"))); + } + + @Test + public void testHttpBodyExceedsLimit() throws Exception { + Assert.assertEquals(413, + post(httpServerUri, new StringEntity(repeat('a', HTTP_MAX_BODY_SIZE + 1)))); + } + + @Test + public void testHttpBodyAtExactLimit() throws Exception { + Assert.assertEquals(200, + post(httpServerUri, new StringEntity(repeat('b', HTTP_MAX_BODY_SIZE)))); + } + + @Test + public void testTwoServicesHaveIndependentLimits() throws Exception { + // A body that exceeds secondService limit but is within httpService limit + String body = repeat('d', SECOND_SERVICE_MAX_BODY_SIZE + 100); + Assert.assertTrue(body.length() < HTTP_MAX_BODY_SIZE); + + Assert.assertEquals(200, post(httpServerUri, new StringEntity(body))); + Assert.assertEquals(413, post(secondServerUri, new StringEntity(body))); + } + + @Test + public void testLimitIsBasedOnBytesNotCharacters() throws Exception { + // Each CJK character is 3 UTF-8 bytes; 342 chars x 3 = 1026 bytes > 1024 + String cjk = repeat('一', 342); + Assert.assertEquals(342, cjk.length()); + Assert.assertEquals(1026, cjk.getBytes("UTF-8").length); + Assert.assertEquals(413, post(httpServerUri, new StringEntity(cjk, "UTF-8"))); + } + + /** + * Chunked request within the limit should succeed. + * InputStreamEntity with size=-1 sends chunked Transfer-Encoding (no Content-Length). + */ + @Test + public void testChunkedBodyWithinLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE / 4).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + Assert.assertEquals(200, post(httpServerUri, chunked)); + } + + /** + * Chunked oversized body hitting a servlet with broad catch(Exception). + * + * SizeLimitHandler's LimitInterceptor throws BadMessageException during + * streaming read, but the servlet's catch(Exception) absorbs it and returns + * 200 + error JSON instead of 413. This matches real TRON servlet behavior. + * + * OOM protection still works: the body read is truncated at the limit. + */ + @Test + public void testChunkedBodyExceedsLimit() throws Exception { + byte[] data = repeat('a', HTTP_MAX_BODY_SIZE * 2).getBytes("UTF-8"); + InputStreamEntity chunked = new InputStreamEntity(new ByteArrayInputStream(data), -1); + HttpPost req = new HttpPost(httpServerUri); + req.setEntity(chunked); + HttpResponse resp = client.execute(req); + int status = resp.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(resp.getEntity()); + logger.info("Chunked oversized: status={}, body={}", status, body); + + // catch(Exception) absorbs BadMessageException -> 200 + error JSON, not 413. + // Body read IS truncated - OOM protection still effective. + Assert.assertEquals(200, status); + Assert.assertTrue("Error should be surfaced in response body", + body.contains("Error")); + } + + /** + * When maxRequestSize is 0, SizeLimitHandler treats it as "reject all bodies > 0 bytes". + * Jetty's logic: {@code _requestLimit >= 0 && size > _requestLimit} - 0 >= 0 is true, + * so any non-empty body triggers 413. This is NOT "pass all" - it is a silent DoS + * against the node's own API. + */ + @Test + public void testZeroLimitRejectsAllBodies() throws Exception { + int zeroPort = PublicMethod.chooseRandomPort(); + TestHttpService zeroService = new TestHttpService(zeroPort, 0); + try { + zeroService.start().get(10, TimeUnit.SECONDS); + URI zeroUri = new URI(String.format("http://localhost:%d/", zeroPort)); + + // Empty body should pass (0 is NOT > 0) + Assert.assertEquals(200, post(zeroUri, new StringEntity(""))); + + // Any non-empty body should be rejected + Assert.assertEquals(413, post(zeroUri, new StringEntity("x"))); + } finally { + zeroService.stop(); + } + } + + /** + * For pure ASCII JSON (the normal TRON API case), wire bytes and + * {@code body.getBytes().length} (what {@code Util.checkBodySize()} measures) + * must be identical - the two enforcement layers agree exactly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForAsciiJson() throws Exception { + String jsonBody = "{\"owner_address\":\"TN3zfjYUmMFK3ZsHSsrdJoNRtGkQmZLBLz\"" + + ",\"amount\":1000000}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for ASCII JSON", + wireBytes, servletBytes); + } + + /** + * For UTF-8 JSON with multi-byte characters (CJK), wire bytes and + * {@code body.getBytes().length} must still be identical - UTF-8 round-trips + * through {@code request.getReader()} -> {@code String.getBytes()} losslessly. + */ + @Test + public void testWireBytesMatchCheckBodySizeForUtf8Json() throws Exception { + String jsonBody = "{\"name\":\"测试地址\",\"amount\":100}"; + int wireBytes = jsonBody.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(jsonBody, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertEquals("wire bytes should equal checkBodySize for UTF-8 JSON", + wireBytes, servletBytes); + } + + /** + * When the body contains {@code \r\n} line endings, {@code lines().collect()} + * normalizes them to {@code \n} (on Linux) or the platform line separator. + * This makes {@code checkBodySize} measure fewer bytes than the wire - + * a safe direction: checkBodySize never rejects what SizeLimitHandler accepts. + */ + @Test + public void testCheckBodySizeSafeDirectionWithNewlines() throws Exception { + String body = "{\"key1\":\"value1\",\r\n\"key2\":\"value2\",\r\n\"key3\":\"value3\"}"; + int wireBytes = body.getBytes("UTF-8").length; + + String respBody = postForBody(httpServerUri, new StringEntity(body, "UTF-8")); + JSONObject json = JSONObject.parseObject(respBody); + int servletBytes = json.getIntValue("bytes"); + + Assert.assertTrue("checkBodySize bytes <= wire bytes (safe direction)", + servletBytes <= wireBytes); + logger.info("Newline test: wire={}, servlet={}, diff={}", + wireBytes, servletBytes, wireBytes - servletBytes); + } + + /** POSTs with the given entity and returns the response body as a string. */ + private String postForBody(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + return EntityUtils.toString(resp.getEntity()); + } + + /** POSTs with the given entity and returns the HTTP status code. */ + private int post(URI uri, HttpEntity entity) throws Exception { + HttpPost req = new HttpPost(uri); + req.setEntity(entity); + HttpResponse resp = client.execute(req); + EntityUtils.consume(resp.getEntity()); + return resp.getStatusLine().getStatusCode(); + } + + /** Returns a string of {@code n} repetitions of {@code c}. */ + private static String repeat(char c, int n) { + return new String(new char[n]).replace('\0', c); + } +} diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java index 34a8e82c424..9b0f17244a8 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserJsonTest.java @@ -65,7 +65,6 @@ public synchronized void testEventParser() { for (int i = 0; i < entryArr.size(); i++) { JSONObject e = entryArr.getJSONObject(i); - System.out.println(e.getString("name")); if (e.getString("name") != null) { if (e.getString("name").equalsIgnoreCase("eventBytesL")) { entry = e; diff --git a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java index eff644b9cd9..0d2a8ed1496 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -68,7 +68,6 @@ public synchronized void testEventParser() { ABI.Entry entry = null; for (ABI.Entry e : abi.getEntrysList()) { - System.out.println(e.getName()); if (e.getName().equalsIgnoreCase("eventBytesL")) { entry = e; break; diff --git a/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java b/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java index 5ee32d98ee6..c87d8e1136e 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/FilterQueryTest.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.tron.common.logsfilter.capsule.ContractEventTriggerCapsule; @@ -28,6 +29,11 @@ @Slf4j public class FilterQueryTest { + @After + public void tearDown() { + EventPluginLoader.getInstance().setFilterQuery(null); + } + @Test public synchronized void testParseFilterQueryBlockNumber() { assertEquals(LATEST_BLOCK_NUM, parseToBlockNumber(EMPTY)); diff --git a/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java b/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java index d356e43d66c..5219654977b 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/NativeMessageQueueTest.java @@ -1,7 +1,10 @@ package org.tron.common.logsfilter; +import java.util.concurrent.ExecutorService; +import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.logsfilter.nativequeue.NativeMessageQueue; import org.zeromq.SocketType; import org.zeromq.ZContext; @@ -13,6 +16,15 @@ public class NativeMessageQueueTest { public String dataToSend = "################"; public String topic = "testTopic"; + private ExecutorService subscriberExecutor; + private final String zmqSubscriber = "zmq-subscriber"; + + @After + public void tearDown() { + ExecutorServiceManager.shutdownAndAwaitTermination(subscriberExecutor, zmqSubscriber); + subscriberExecutor = null; + } + @Test public void invalidBindPort() { boolean bRet = NativeMessageQueue.getInstance().start(-1111, 0); @@ -39,7 +51,7 @@ public void publishTrigger() { try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } NativeMessageQueue.getInstance().publishTrigger(dataToSend, topic); @@ -47,14 +59,15 @@ public void publishTrigger() { try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } NativeMessageQueue.getInstance().stop(); } public void startSubscribeThread() { - Thread thread = new Thread(() -> { + subscriberExecutor = ExecutorServiceManager.newSingleThreadExecutor(zmqSubscriber); + subscriberExecutor.execute(() -> { try (ZContext context = new ZContext()) { ZMQ.Socket subscriber = context.createSocket(SocketType.SUB); @@ -70,6 +83,5 @@ public void startSubscribeThread() { // ZMQ.Socket will be automatically closed when ZContext is closed } }); - thread.start(); } } diff --git a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java index 5381c6ab2de..b5f7e676eea 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/capsule/BlockFilterCapsuleTest.java @@ -22,7 +22,6 @@ public void setUp() { public void testSetAndGetBlockHash() { blockFilterCapsule .setBlockHash("e58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f"); - System.out.println(blockFilterCapsule); Assert.assertEquals("e58f33f9baf9305dc6f82b9f1934ea8f0ade2defb951258d50167028c780351f", blockFilterCapsule.getBlockHash()); } 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 40a4003f625..8e38c08c4d8 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.parameter.CommonParameter; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.Commons; @@ -153,8 +154,13 @@ public void testSuccess() { @Test public void testSuccessNoBandd() { + boolean originalDebug = CommonParameter.getInstance().isDebug(); try { byte[] contractAddress = createContract(); + // Enable debug mode to bypass CPU time limit check in Program.checkCPUTimeLimit(). + // Without this, the heavy contract execution (setCoin) may exceed the time threshold + // on slow machines and cause the test to fail non-deterministically. + CommonParameter.getInstance().setDebug(true); TriggerSmartContract triggerContract = TvmTestUtils.createTriggerContract(contractAddress, "setCoin(uint256)", "50", false, 0, Commons.decodeFromBase58Check(TriggerOwnerTwoAddress)); @@ -185,6 +191,8 @@ public void testSuccessNoBandd() { balance); } catch (TronException e) { Assert.assertNotNull(e); + } finally { + CommonParameter.getInstance().setDebug(originalDebug); } } @@ -254,4 +262,4 @@ public void testMaxContractResultSize() { } Assert.assertEquals(2, maxSize); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java b/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java index 0d6305f8782..071c07bdc9e 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/FreezeTest.java @@ -289,17 +289,9 @@ public void testWithCallerEnergyChangedInTx() throws Exception { TVMTestResult result = freezeForOther(userA, contractAddr, userA, frozenBalance, 1); - System.out.println(result.getReceipt().getEnergyUsageTotal()); - System.out.println(accountStore.get(userA)); - System.out.println(accountStore.get(owner)); - clearDelegatedExpireTime(contractAddr, userA); result = unfreezeForOther(userA, contractAddr, userA, 1); - - System.out.println(result.getReceipt().getEnergyUsageTotal()); - System.out.println(accountStore.get(userA)); - System.out.println(accountStore.get(owner)); } @Test diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java index 66de45a0658..75b11f4ab9d 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java @@ -124,8 +124,6 @@ public void testZeroLengthOneArray() { // witness array zero, amount array non-zero Program program = mockProgram(0, 0, 64, 1, 0); long cost = EnergyCost.getVoteWitnessCost3(program); - // witnessArraySize = 0 * 32 + 32 = 32, witnessMemNeeded = 0 + 32 = 32 - // amountArraySize = 1 * 32 + 32 = 64, amountMemNeeded = 64 + 64 = 128 // memWords = 128 / 32 = 4 // memEnergy = 3 * 4 + 4 * 4 / 512 = 12 assertEquals(30012, cost); diff --git a/framework/src/test/java/org/tron/common/utils/PropUtilTest.java b/framework/src/test/java/org/tron/common/utils/PropUtilTest.java index 2df5bd8effd..bc4d15a4df7 100644 --- a/framework/src/test/java/org/tron/common/utils/PropUtilTest.java +++ b/framework/src/test/java/org/tron/common/utils/PropUtilTest.java @@ -1,21 +1,16 @@ package org.tron.common.utils; import java.io.File; -import java.io.IOException; import org.junit.Assert; import org.junit.Test; public class PropUtilTest { @Test - public void testWriteProperty() { + public void testWriteProperty() throws Exception { String filename = "test_prop.properties"; File file = new File(filename); - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } + file.createNewFile(); PropUtil.writeProperty(filename, "key", "value"); Assert.assertTrue("value".equals(PropUtil.readProperty(filename, "key"))); PropUtil.writeProperty(filename, "key", "value2"); @@ -24,17 +19,13 @@ public void testWriteProperty() { } @Test - public void testReadProperty() { + public void testReadProperty() throws Exception { String filename = "test_prop.properties"; File file = new File(filename); - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } + file.createNewFile(); PropUtil.writeProperty(filename, "key", "value"); Assert.assertTrue("value".equals(PropUtil.readProperty(filename, "key"))); file.delete(); Assert.assertTrue("".equals(PropUtil.readProperty(filename, "key"))); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/ShieldWalletTest.java b/framework/src/test/java/org/tron/core/ShieldWalletTest.java index 903309510a8..b9fa48dca38 100644 --- a/framework/src/test/java/org/tron/core/ShieldWalletTest.java +++ b/framework/src/test/java/org/tron/core/ShieldWalletTest.java @@ -369,14 +369,12 @@ public void testCreateShieldedContractParameters2() throws ContractExeException Assert.fail(); } - try { - wallet1.createShieldedContractParameters(builder.build()); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("PaymentAddress in ReceiveNote should not be empty", - e.getMessage()); - } + PrivateShieldedTRC20Parameters.Builder finalBuilder = builder; + Exception e1 = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(finalBuilder.build())); + Assert.assertTrue(e1 instanceof ContractValidateException); + Assert.assertEquals("PaymentAddress in ReceiveNote should not be empty", + e1.getMessage()); String parameter2 = new String(ByteArray.fromHexString( "7b0a202020202261736b223a2263323531336539653330383439343933326264383265306365353336" @@ -402,14 +400,12 @@ public void testCreateShieldedContractParameters2() throws ContractExeException Assert.fail(); } - try { - wallet1.createShieldedContractParameters(builder.build()); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("PaymentAddress in SpendNote should not be empty", - e.getMessage()); - } + PrivateShieldedTRC20Parameters.Builder finalBuilder1 = builder; + Exception e2 = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(finalBuilder1.build())); + Assert.assertTrue(e2 instanceof ContractValidateException); + Assert.assertEquals("PaymentAddress in SpendNote should not be empty", + e2.getMessage()); } @Test diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index 44e25a16387..0df8d6cdc2c 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -860,15 +860,12 @@ public void testGetDelegatedResourceV2() { @Test public void testGetPaginatedNowWitnessList_Error() { - try { - // To avoid throw MaintenanceClearingException - dbManager.getChainBaseManager().getDynamicPropertiesStore().saveStateFlag(1); - wallet.getPaginatedNowWitnessList(0, 10); - Assert.fail("Should throw error when in maintenance period"); - } catch (Exception e) { - Assert.assertTrue("Should throw MaintenanceClearingException", - e instanceof MaintenanceUnavailableException); - } + // To avoid throw MaintenanceClearingException + dbManager.getChainBaseManager().getDynamicPropertiesStore().saveStateFlag(1); + Exception maintenanceEx = Assert.assertThrows(Exception.class, + () -> wallet.getPaginatedNowWitnessList(0, 10)); + Assert.assertTrue("Should throw MaintenanceClearingException", + maintenanceEx instanceof MaintenanceUnavailableException); try { Args.getInstance().setSolidityNode(true); @@ -1376,13 +1373,9 @@ public void testEstimateEnergyOutOfTime() { Args.getInstance().setEstimateEnergy(true); - try { - wallet.estimateEnergy( - contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder); - Assert.fail("EstimateEnergy should throw exception!"); - } catch (Program.OutOfTimeException ignored) { - Assert.assertTrue(true); - } + Assert.assertThrows(Program.OutOfTimeException.class, + () -> wallet.estimateEnergy( + contract, trxCap, trxExtBuilder, retBuilder, estimateBuilder)); } @Test diff --git a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java index ddcb9976200..c830cd091e6 100644 --- a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceActuatorTest.java @@ -22,7 +22,6 @@ import org.tron.core.capsule.DelegatedResourceAccountIndexCapsule; import org.tron.core.capsule.DelegatedResourceCapsule; import org.tron.core.capsule.TransactionResultCapsule; -import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.config.args.Args; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; @@ -621,35 +620,6 @@ public void frozenNumTest() { } } - //@Test - public void moreThanFrozenNumber() { - long frozenBalance = 1_000_000_000L; - long duration = 3; - FreezeBalanceActuator actuator = new FreezeBalanceActuator(); - actuator.setChainBaseManager(dbManager.getChainBaseManager()) - .setAny(getContractForBandwidth(OWNER_ADDRESS, frozenBalance, duration)); - - TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - actuator.execute(ret); - } catch (ContractValidateException | ContractExeException e) { - Assert.fail(); - } - try { - actuator.validate(); - actuator.execute(ret); - fail("cannot run here."); - } catch (ContractValidateException e) { - long maxFrozenNumber = ChainConstant.MAX_FROZEN_NUMBER; - Assert.assertEquals("max frozen number is: " + maxFrozenNumber, e.getMessage()); - - } catch (ContractExeException e) { - Assert.fail(); - } - } - - @Test public void commonErrorCheck() { FreezeBalanceActuator actuator = new FreezeBalanceActuator(); diff --git a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java index 24585326110..92e7cfa78ca 100644 --- a/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/FreezeBalanceV2ActuatorTest.java @@ -268,33 +268,6 @@ public void lessThan1TrxTest() { } } - //@Test - public void moreThanFrozenNumber() { - long frozenBalance = 1_000_000_000L; - FreezeBalanceActuator actuator = new FreezeBalanceActuator(); - actuator.setChainBaseManager(dbManager.getChainBaseManager()) - .setAny(getContractV2ForBandwidth(OWNER_ADDRESS, frozenBalance)); - - TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - actuator.execute(ret); - } catch (ContractValidateException | ContractExeException e) { - Assert.fail(); - } - try { - actuator.validate(); - actuator.execute(ret); - fail("cannot run here."); - } catch (ContractValidateException e) { - long maxFrozenNumber = ChainConstant.MAX_FROZEN_NUMBER; - Assert.assertEquals("max frozen number is: " + maxFrozenNumber, e.getMessage()); - } catch (ContractExeException e) { - Assert.fail(); - } - } - - @Test public void commonErrorCheck() { FreezeBalanceV2Actuator actuator = new FreezeBalanceV2Actuator(); diff --git a/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java index d2ec8c30994..4966ef67987 100644 --- a/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/MarketCancelOrderActuatorTest.java @@ -188,12 +188,9 @@ public void invalidOwnerAddress() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_INVALID, orderId)); - try { - actuator.validate(); - Assert.fail("Invalid address"); - } catch (ContractValidateException e) { - Assert.assertEquals("Invalid address", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Invalid address", e.getMessage()); } /** @@ -208,12 +205,9 @@ public void notExistAccount() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_NOT_EXIST, orderId)); - try { - actuator.validate(); - Assert.fail("Account does not exist!"); - } catch (ContractValidateException e) { - Assert.assertEquals("Account does not exist!", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Account does not exist!", e.getMessage()); } /** @@ -227,12 +221,9 @@ public void notExistOrder() { MarketCancelOrderActuator actuator = new MarketCancelOrderActuator(); actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_FIRST, orderId)); - try { - actuator.validate(); - Assert.fail("orderId not exists"); - } catch (ContractValidateException e) { - Assert.assertEquals("orderId not exists", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("orderId not exists", e.getMessage()); } /** @@ -261,12 +252,9 @@ public void orderNotActive() throws Exception { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny(getContract( OWNER_ADDRESS_FIRST, orderId)); - try { - actuator.validate(); - Assert.fail("Order is not active!"); - } catch (ContractValidateException e) { - Assert.assertEquals("Order is not active!", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("Order is not active!", e.getMessage()); } diff --git a/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java index f3211c8b8eb..344a4e95f30 100644 --- a/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/UnDelegateResourceActuatorTest.java @@ -464,29 +464,25 @@ public void testLockedUnDelegateBalanceForBandwidthInsufficient() { actuator.setChainBaseManager(dbManager.getChainBaseManager()).setAny( getDelegatedContractForBandwidth(OWNER_ADDRESS, delegateBalance)); - try { - ownerCapsule = dbManager.getAccountStore().get(owner); - Assert.assertEquals(delegateBalance, - receiverCapsule.getAcquiredDelegatedFrozenV2BalanceForBandwidth()); - Assert.assertEquals(delegateBalance, ownerCapsule.getDelegatedFrozenV2BalanceForBandwidth()); - Assert.assertEquals(0, ownerCapsule.getFrozenV2BalanceForBandwidth()); - Assert.assertEquals(delegateBalance, ownerCapsule.getTronPower()); - Assert.assertEquals(1_000_000_000, ownerCapsule.getNetUsage()); - Assert.assertEquals(1_000_000_000, receiverCapsule.getNetUsage()); - DelegatedResourceCapsule delegatedResourceCapsule = dbManager.getDelegatedResourceStore() - .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, false)); - DelegatedResourceCapsule lockedResourceCapsule = dbManager.getDelegatedResourceStore() - .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, true)); - Assert.assertNull(delegatedResourceCapsule); - Assert.assertNotNull(lockedResourceCapsule); - - actuator.validate(); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof ContractValidateException); - Assert.assertEquals("insufficient delegatedFrozenBalance(BANDWIDTH), " - + "request=1000000000, unlock_balance=0", e.getMessage()); - } + ownerCapsule = dbManager.getAccountStore().get(owner); + Assert.assertEquals(delegateBalance, + receiverCapsule.getAcquiredDelegatedFrozenV2BalanceForBandwidth()); + Assert.assertEquals(delegateBalance, ownerCapsule.getDelegatedFrozenV2BalanceForBandwidth()); + Assert.assertEquals(0, ownerCapsule.getFrozenV2BalanceForBandwidth()); + Assert.assertEquals(delegateBalance, ownerCapsule.getTronPower()); + Assert.assertEquals(1_000_000_000, ownerCapsule.getNetUsage()); + Assert.assertEquals(1_000_000_000, receiverCapsule.getNetUsage()); + DelegatedResourceCapsule delegatedResourceCapsule = dbManager.getDelegatedResourceStore() + .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, false)); + DelegatedResourceCapsule lockedResourceCapsule = dbManager.getDelegatedResourceStore() + .get(DelegatedResourceCapsule.createDbKeyV2(owner, receiver, true)); + Assert.assertNull(delegatedResourceCapsule); + Assert.assertNotNull(lockedResourceCapsule); + + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("insufficient delegatedFrozenBalance(BANDWIDTH), " + + "request=1000000000, unlock_balance=0", e.getMessage()); } @Test @@ -976,22 +972,16 @@ public void noDelegateBalance() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getDelegatedContractForBandwidth(OWNER_ADDRESS, delegateBalance)); - try { - actuator.validate(); - Assert.fail("cannot run here."); - } catch (ContractValidateException e) { - Assert.assertEquals("delegated Resource does not exist", e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("delegated Resource does not exist", e1.getMessage()); actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getDelegatedContractForCpu(delegateBalance)); - try { - actuator.validate(); - Assert.fail("cannot run here."); - } catch (ContractValidateException e) { - Assert.assertEquals("delegated Resource does not exist", e.getMessage()); - } + ContractValidateException e2 = + Assert.assertThrows(ContractValidateException.class, () -> actuator.validate()); + Assert.assertEquals("delegated Resource does not exist", e2.getMessage()); } @Test diff --git a/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java index f5c65bf381f..7f74ee3fcc5 100644 --- a/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/UnfreezeBalanceActuatorTest.java @@ -1166,12 +1166,9 @@ public void testUnfreezeBalanceForTronPowerWithOldTronPowerAfterNewResourceModel actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContractForTronPower(OWNER_ADDRESS)); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("It's not time to unfreeze(TronPower).", e.getMessage()); - } + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); + Assert.assertEquals("It's not time to unfreeze(TronPower).", e.getMessage()); } } diff --git a/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java index 6ec72043722..9823c3aba51 100644 --- a/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/VoteWitnessActuatorTest.java @@ -569,12 +569,8 @@ public void voteWitnessWithoutEnoughOldTronPowerAfterNewResourceModel() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContract(OWNER_ADDRESS, WITNESS_ADDRESS, 100L)); TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertTrue(e instanceof ContractValidateException); - } + Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(0L); } @@ -658,12 +654,8 @@ public void voteWitnessWithoutEnoughOldAndNewTronPowerAfterNewResourceModel() { actuator.setChainBaseManager(dbManager.getChainBaseManager()) .setAny(getContract(OWNER_ADDRESS, WITNESS_ADDRESS, 4000000L)); TransactionResultCapsule ret = new TransactionResultCapsule(); - try { - actuator.validate(); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertTrue(e instanceof ContractValidateException); - } + Assert.assertThrows(ContractValidateException.class, + () -> actuator.validate()); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(0L); } diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index f8d8e6bdd9d..c26da5a7bf7 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -62,15 +62,12 @@ public void validProposalTypeCheck() throws ContractValidateException { Assert.assertNull(ProposalType.getEnumOrNull(-2)); Assert.assertEquals(ProposalType.ALLOW_TVM_SOLIDITY_059, ProposalType.getEnumOrNull(32)); - long code = -1; - try { - ProposalType.getEnum(code); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Does not support code : " + code, e.getMessage()); - } + long finalCode = -1; + ContractValidateException e = Assert.assertThrows(ContractValidateException.class, + () -> ProposalType.getEnum(finalCode)); + Assert.assertEquals("Does not support code : " + finalCode, e.getMessage()); - code = 32; + long code = 32; Assert.assertEquals(ProposalType.ALLOW_TVM_SOLIDITY_059, ProposalType.getEnum(code)); } @@ -79,217 +76,145 @@ public void validProposalTypeCheck() throws ContractValidateException { public void validateCheck() { long invalidValue = -1; - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ACCOUNT_UPGRADE_COST.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ACCOUNT_UPGRADE_COST.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_ACCOUNT_FEE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_ACCOUNT_FEE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ASSET_ISSUE_FEE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ASSET_ISSUE_FEE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), invalidValue); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), LONG_VALUE + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals(LONG_VALUE_ERROR, e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 3 * 27 * 1000 - 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 24 * 3600 * 1000 + 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_CREATION_OF_CONTRACTS.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_CREATION_OF_CONTRACTS] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ACCOUNT_UPGRADE_COST.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e1.getMessage()); + + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ACCOUNT_UPGRADE_COST.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e2.getMessage()); + + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_ACCOUNT_FEE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e3.getMessage()); + + ContractValidateException e4 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_ACCOUNT_FEE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e4.getMessage()); + + ContractValidateException e5 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ASSET_ISSUE_FEE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e5.getMessage()); + + ContractValidateException e6 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ASSET_ISSUE_FEE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e6.getMessage()); + + ContractValidateException e7 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e7.getMessage()); + + ContractValidateException e8 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_PAY_PER_BLOCK.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e8.getMessage()); + + ContractValidateException e9 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e9.getMessage()); + + ContractValidateException e10 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.WITNESS_STANDBY_ALLOWANCE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e10.getMessage()); + + ContractValidateException e11 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e11.getMessage()); + + ContractValidateException e12 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_FEE_IN_SYSTEM_CONTRACT.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e12.getMessage()); + + ContractValidateException e13 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), invalidValue)); + Assert.assertEquals(LONG_VALUE_ERROR, e13.getMessage()); + + ContractValidateException e14 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CREATE_NEW_ACCOUNT_BANDWIDTH_RATE.getCode(), LONG_VALUE + 1)); + Assert.assertEquals(LONG_VALUE_ERROR, e14.getMessage()); + + ContractValidateException e15 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 3 * 27 * 1000 - 1)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", + e15.getMessage()); + + ContractValidateException e16 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAINTENANCE_TIME_INTERVAL.getCode(), 24 * 3600 * 1000 + 1)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [3 * 27 * 1000,24 * 3600 * 1000]", + e16.getMessage()); + + ContractValidateException e17 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_CREATION_OF_CONTRACTS.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_CREATION_OF_CONTRACTS] is only allowed to be 1", + e17.getMessage()); dynamicPropertiesStore = dbManager.getDynamicPropertiesStore(); dynamicPropertiesStore.saveRemoveThePowerOfTheGr(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[REMOVE_THE_POWER_OF_THE_GR] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e18 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 2)); + Assert.assertEquals( + "This value[REMOVE_THE_POWER_OF_THE_GR] is only allowed to be 1", + e18.getMessage()); dynamicPropertiesStore.saveRemoveThePowerOfTheGr(-1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This proposal has been executed before and is only allowed to be executed once", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 9); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [10,100]", e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 101); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter value, valid range is [10,100]", e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_DELEGATE_RESOURCE.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_DELEGATE_RESOURCE] is only allowed to be 1", e.getMessage()); - } + ContractValidateException e19 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.REMOVE_THE_POWER_OF_THE_GR.getCode(), 1)); + Assert.assertEquals( + "This proposal has been executed before and is only allowed to be executed once", + e19.getMessage()); + + ContractValidateException e20 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 9)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [10,100]", e20.getMessage()); + + ContractValidateException e21 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.MAX_CPU_TIME_OF_ONE_TX.getCode(), 101)); + Assert.assertEquals( + "Bad chain parameter value, valid range is [10,100]", e21.getMessage()); + + ContractValidateException e22 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_DELEGATE_RESOURCE.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_DELEGATE_RESOURCE] is only allowed to be 1", e22.getMessage()); dynamicPropertiesStore.saveAllowSameTokenName(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_TRANSFER_TRC10] is only allowed to be 1", e.getMessage()); - } + ContractValidateException e23 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_TRANSFER_TRC10] is only allowed to be 1", e23.getMessage()); dynamicPropertiesStore.saveAllowSameTokenName(0); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("[ALLOW_SAME_TOKEN_NAME] proposal must be approved " - + "before [ALLOW_TVM_TRANSFER_TRC10] can be proposed", e.getMessage()); - } + ContractValidateException e24 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_TRANSFER_TRC10.getCode(), 1)); + Assert.assertEquals("[ALLOW_SAME_TOKEN_NAME] proposal must be approved " + + "before [ALLOW_TVM_TRANSFER_TRC10] can be proposed", e24.getMessage()); forkUtils.init(dbManager.getChainBaseManager()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() @@ -308,15 +233,12 @@ public void validateCheck() { List w = new ArrayList<>(); w.add(address); forkUtils.getManager().getWitnessScheduleStore().saveActiveWitnesses(w); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_SHIELDED_TRC20_TRANSACTION - .getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("This value[ALLOW_SHIELDED_TRC20_TRANSACTION] is only allowed" - + " to be 1 or 0", e.getMessage()); - } + ContractValidateException e25 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_SHIELDED_TRC20_TRANSACTION + .getCode(), 2)); + Assert.assertEquals("This value[ALLOW_SHIELDED_TRC20_TRANSACTION] is only allowed" + + " to be 1 or 0", e25.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_3.getHardForkTime() - 1) / maintenanceTimeInterval + 1) @@ -325,33 +247,24 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_3.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.FREE_NET_LIMIT - .getCode(), -1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Bad chain parameter value, valid range is [0,100_000]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.TOTAL_NET_LIMIT.getCode(), -1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals("Bad chain parameter value, valid range is [0, 1_000_000_000_000L]", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_OLD_REWARD_OPT]", - e.getMessage()); - } + ContractValidateException e26 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.FREE_NET_LIMIT + .getCode(), -1)); + Assert.assertEquals("Bad chain parameter value, valid range is [0,100_000]", + e26.getMessage()); + + ContractValidateException e27 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.TOTAL_NET_LIMIT.getCode(), -1)); + Assert.assertEquals("Bad chain parameter value, valid range is [0, 1_000_000_000_000L]", + e27.getMessage()); + + ContractValidateException e28 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_OLD_REWARD_OPT]", + e28.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_7_4.getHardForkTime() - 1) / maintenanceTimeInterval + 1) * maintenanceTimeInterval; @@ -359,47 +272,35 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_4.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_OLD_REWARD_OPT] is only allowed to be 1", - e.getMessage()); - } - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_NEW_REWARD] or [ALLOW_TVM_VOTE] proposal must be approved " - + "before [ALLOW_OLD_REWARD_OPT] can be proposed", - e.getMessage()); - } + ContractValidateException e29 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_OLD_REWARD_OPT] is only allowed to be 1", + e29.getMessage()); + ContractValidateException e30 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_NEW_REWARD] or [ALLOW_TVM_VOTE] proposal must be approved " + + "before [ALLOW_OLD_REWARD_OPT] can be proposed", + e30.getMessage()); dynamicPropertiesStore.put("NEW_REWARD_ALGORITHM_EFFECTIVE_CYCLE".getBytes(), new BytesCapsule(ByteArray.fromLong(4000))); dynamicPropertiesStore.saveAllowOldRewardOpt(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_OLD_REWARD_OPT] has been valid, no need to propose again", - e.getMessage()); - } - - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_STRICT_MATH]", - e.getMessage()); - } + ContractValidateException e31 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_OLD_REWARD_OPT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_OLD_REWARD_OPT] has been valid, no need to propose again", + e31.getMessage()); + + ContractValidateException e32 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 2)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_STRICT_MATH]", + e32.getMessage()); hardForkTime = ((ForkBlockVersionEnum.VERSION_4_7_7.getHardForkTime() - 1) / maintenanceTimeInterval + 1) * maintenanceTimeInterval; @@ -407,15 +308,12 @@ public void validateCheck() { .saveLatestBlockHeaderTimestamp(hardForkTime + 1); forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_7.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_STRICT_MATH] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e33 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_STRICT_MATH] is only allowed to be 1", + e33.getMessage()); try { ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.ALLOW_STRICT_MATH.getCode(), 1); @@ -426,15 +324,12 @@ public void validateCheck() { ProposalType.ALLOW_STRICT_MATH.getCode(), 1).build(); ProposalCapsule proposalCapsule = new ProposalCapsule(proposal); ProposalService.process(dbManager, proposalCapsule); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_STRICT_MATH.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_STRICT_MATH] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e34 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_STRICT_MATH.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_STRICT_MATH] has been valid, no need to propose again", + e34.getMessage()); testEnergyAdjustmentProposal(); @@ -455,15 +350,12 @@ public void validateCheck() { private void testEnergyAdjustmentProposal() { // Should fail because cannot pass the fork controller check - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_ENERGY_ADJUSTMENT]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_ENERGY_ADJUSTMENT]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -480,15 +372,12 @@ private void testEnergyAdjustmentProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_7_5.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_ENERGY_ADJUSTMENT] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_ENERGY_ADJUSTMENT] is only allowed to be 1", + e2.getMessage()); // Should succeed try { @@ -504,27 +393,21 @@ private void testEnergyAdjustmentProposal() { proposalCapsule.setParameters(parameter); ProposalService.process(dbManager, proposalCapsule); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_ENERGY_ADJUSTMENT] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_ENERGY_ADJUSTMENT.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_ENERGY_ADJUSTMENT] has been valid, no need to propose again", + e3.getMessage()); } private void testConsensusLogicOptimizationProposal() { - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [CONSENSUS_LOGIC_OPTIMIZATION]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [CONSENSUS_LOGIC_OPTIMIZATION]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -541,26 +424,20 @@ private void testConsensusLogicOptimizationProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[CONSENSUS_LOGIC_OPTIMIZATION] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 2)); + Assert.assertEquals( + "This value[CONSENSUS_LOGIC_OPTIMIZATION] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveConsensusLogicOptimization(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[CONSENSUS_LOGIC_OPTIMIZATION] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.CONSENSUS_LOGIC_OPTIMIZATION.getCode(), 1)); + Assert.assertEquals( + "[CONSENSUS_LOGIC_OPTIMIZATION] has been valid, no need to propose again", + e3.getMessage()); } @@ -568,15 +445,12 @@ private void testAllowTvmCancunProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_CANCUN]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_CANCUN]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -593,26 +467,20 @@ private void testAllowTvmCancunProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_CANCUN] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_CANCUN] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmCancun(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_CANCUN.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_CANCUN] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_CANCUN.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_CANCUN] has been valid, no need to propose again", + e3.getMessage()); } @@ -620,15 +488,12 @@ private void testAllowTvmBlobProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_BLOB]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_BLOB]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -645,26 +510,20 @@ private void testAllowTvmBlobProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_0.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_BLOB] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_BLOB] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmBlob(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_BLOB.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_BLOB] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_BLOB.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_BLOB] has been valid, no need to propose again", + e3.getMessage()); } @@ -672,15 +531,12 @@ private void testAllowTvmSelfdestructRestrictionProposal() { byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "Bad chain parameter id [ALLOW_TVM_SELFDESTRUCT_RESTRICTION]", - e.getMessage()); - } + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_SELFDESTRUCT_RESTRICTION]", + e1.getMessage()); long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() .getMaintenanceTimeInterval(); @@ -697,26 +553,20 @@ private void testAllowTvmSelfdestructRestrictionProposal() { .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); // Should fail because the proposal value is invalid - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 2); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "This value[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] is only allowed to be 1", - e.getMessage()); - } + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] is only allowed to be 1", + e2.getMessage()); dynamicPropertiesStore.saveAllowTvmSelfdestructRestriction(1); - try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, - ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1); - Assert.fail(); - } catch (ContractValidateException e) { - Assert.assertEquals( - "[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] has been valid, no need to propose again", - e.getMessage()); - } + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_SELFDESTRUCT_RESTRICTION.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_TVM_SELFDESTRUCT_RESTRICTION] has been valid, no need to propose again", + e3.getMessage()); } private void testAllowMarketTransaction() { diff --git a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java index df84433726e..88e95f9653e 100644 --- a/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java +++ b/framework/src/test/java/org/tron/core/capsule/utils/MerkleTreeTest.java @@ -101,12 +101,9 @@ private static int getRank(int num) { */ public void test0HashNum() { List hashList = getHash(0); //Empty list. - try { - MerkleTree.getInstance().createTree(hashList); - Assert.assertFalse(true); - } catch (Exception e) { - Assert.assertTrue(e instanceof IndexOutOfBoundsException); - } + Exception e = Assert.assertThrows(Exception.class, + () -> MerkleTree.getInstance().createTree(hashList)); + Assert.assertTrue(e instanceof IndexOutOfBoundsException); } @Test diff --git a/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java b/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java index 500e7454dbe..9c2e8550634 100644 --- a/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java +++ b/framework/src/test/java/org/tron/core/capsule/utils/RLPListTest.java @@ -69,12 +69,8 @@ public void testToBytes() Assert.assertArrayEquals(kBytes, lBytes); char c = 'a'; - try { - method.invoke(RLP.class, c); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(true); - } + Assert.assertThrows(Exception.class, + () -> method.invoke(RLP.class, c)); } @Test 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 88d893bcf97..4835bdf7c13 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,6 +17,7 @@ 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 +40,7 @@ import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; import org.tron.core.config.Configuration; +import org.tron.core.exception.TronError; @Slf4j public class ArgsTest { @@ -119,6 +121,8 @@ public void get() { Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getHttpMaxMessageSize()); + Assert.assertEquals(GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE, parameter.getJsonRpcMaxMessageSize()); Assert.assertEquals(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, parameter.getMaxHeaderListSize()); Assert.assertEquals(1L, parameter.getAllowCreationOfContracts()); Assert.assertEquals(0, parameter.getConsensusLogicOptimization()); @@ -143,14 +147,12 @@ public void testIpFromLibP2p() String configuredExternalIp = parameter.getNodeExternalIp(); Assert.assertEquals("46.168.1.1", configuredExternalIp); - Config config = Configuration.getByFileName(TestConstants.TEST_CONF); - Config config3 = config.withoutPath(ConfigKey.NODE_DISCOVERY_EXTERNAL_IP); - CommonParameter.getInstance().setNodeExternalIp(null); - Method method2 = Args.class.getDeclaredMethod("externalIp", Config.class); + NodeConfig nc = new NodeConfig(); + Method method2 = Args.class.getDeclaredMethod("externalIp", NodeConfig.class); method2.setAccessible(true); - method2.invoke(Args.class, config3); + method2.invoke(Args.class, nc); Assert.assertNotEquals(configuredExternalIp, parameter.getNodeExternalIp()); } @@ -166,7 +168,9 @@ public void testInitService() { Map storage = new HashMap<>(); // avoid the exception for the missing storage storage.put("storage.db.directory", "database"); - Config config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test default value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -193,7 +197,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "true"); storage.put("node.jsonrpc.maxBlockRange", "10"); storage.put("node.jsonrpc.maxSubTopics", "20"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isRpcEnable()); @@ -220,7 +226,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "false"); storage.put("node.jsonrpc.maxBlockRange", "5000"); storage.put("node.jsonrpc.maxSubTopics", "1000"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -247,7 +255,9 @@ public void testInitService() { storage.put("node.jsonrpc.httpPBFTEnable", "true"); storage.put("node.jsonrpc.maxBlockRange", "30"); storage.put("node.jsonrpc.maxSubTopics", "40"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // test value Args.applyConfigParams(config); Assert.assertFalse(Args.getInstance().isRpcEnable()); @@ -265,7 +275,9 @@ public void testInitService() { // test set invalid value storage.put("node.jsonrpc.maxBlockRange", "0"); storage.put("node.jsonrpc.maxSubTopics", "0"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // check value Args.applyConfigParams(config); Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -274,7 +286,9 @@ public void testInitService() { // test set invalid value storage.put("node.jsonrpc.maxBlockRange", "-2"); storage.put("node.jsonrpc.maxSubTopics", "-4"); - config = ConfigFactory.defaultOverrides().withFallback(ConfigFactory.parseMap(storage)); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(storage)) + .withFallback(ConfigFactory.defaultReference()); // check value Args.applyConfigParams(config); Assert.assertEquals(-2, Args.getInstance().getJsonRpcMaxBlockRange()); @@ -360,5 +374,224 @@ public void testConfigStorageDefaults() { Args.clearParam(); } -} + // =========================================================================== + // Boundary tests for clamps applied in Args.java bridge code (not in + // bean postProcess()). + // + // 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. + // =========================================================================== + + @Test + public void testFetchBlockTimeoutClampedBelowMin() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "50"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(100, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } + + @Test + public void testFetchBlockTimeoutClampedAboveMax() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "2000"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(1000, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } + + @Test + public void testFetchBlockTimeoutInRangeUnchanged() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.fetchBlock.timeout", "500"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(500, Args.getInstance().getFetchBlockTimeout()); + Args.clearParam(); + } + + @Test + public void testMaxMessageSizeHumanReadable() { + 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() + .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 * 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"); + config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(configMap)) + .withFallback(ConfigFactory.defaultReference()); + Args.applyConfigParams(config); + Assert.assertEquals(0, Args.getInstance().getMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getHttpMaxMessageSize()); + Assert.assertEquals(0, Args.getInstance().getJsonRpcMaxMessageSize()); + Args.clearParam(); + } + + @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")); + } + + @Test + public void testMaxMessageSizeNonNumeric() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("node.http.maxMessageSize", "abc"); + 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")); + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java new file mode 100644 index 00000000000..80d8287682b --- /dev/null +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerKeystoreTest.java @@ -0,0 +1,207 @@ +package org.tron.core.config.args; + +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 ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.security.SecureRandom; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.LocalWitnesses; +import org.tron.core.exception.TronError; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; + +/** + * Backward compatibility: verifies that keystore files generated by + * the new Toolkit code path can be loaded by WitnessInitializer + * (used by FullNode at startup via localwitnesskeystore config). + */ +public class WitnessInitializerKeystoreTest { + + @ClassRule + public static final TemporaryFolder tempFolder = new TemporaryFolder(); + + // WitnessInitializer prepends user.dir to the filename, so we must + // create the keystore dir relative to user.dir. Use unique name to + // avoid collisions with parallel test runs. + private static final String DIR_NAME = + ".test-keystore-" + System.currentTimeMillis(); + + private static String keystoreFileName; + private static String expectedPrivateKey; + private static final String PASSWORD = "backcompat123"; + + @BeforeClass + public static void setUp() throws Exception { + Args.setParam(new String[]{"-d", tempFolder.newFolder().toString()}, + "config-test.conf"); + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + expectedPrivateKey = ByteArray.toHexString(keyPair.getPrivateKey()); + + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + dir.mkdirs(); + String generatedName = + WalletUtils.generateWalletFile(PASSWORD, keyPair, dir, true); + keystoreFileName = DIR_NAME + "/" + generatedName; + } + + @AfterClass + public static void tearDown() { + Args.clearParam(); + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + if (dir.exists()) { + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + f.delete(); + } + } + dir.delete(); + } + } + + @Test + public void testNewKeystoreLoadableByWitnessInitializer() { + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + + LocalWitnesses result = WitnessInitializer.initFromKeystore( + keystores, PASSWORD, null); + + assertNotNull("WitnessInitializer should load new keystore", result); + assertFalse("Should have at least one private key", + result.getPrivateKeys().isEmpty()); + assertEquals("Private key must match original", + expectedPrivateKey, result.getPrivateKeys().get(0)); + } + + @Test + public void testLegacyTruncationTipFiresOnWhitespacePassword() { + // The SR startup path should mirror the Toolkit's behavior: when the + // supplied password contains whitespace and decryption fails, emit the + // legacy-truncation hint pointing operators at the FullNode keystore- + // factory bug. The Toolkit covers this in + // KeystoreUpdateTest#testUpdateLegacyTipFiresWhenPasswordHasWhitespace. + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + ListAppender appender = attachAppender(); + try { + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore( + keystores, "wrong pass with spaces", null)); + assertEquals(TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + String logs = renderLogs(appender); + assertTrue("Legacy-truncation tip must fire for whitespace password," + + " got: " + logs, + logs.contains("first whitespace-separated word")); + } finally { + detachAppender(appender); + } + } + + @Test + public void testLegacyTruncationTipSuppressedOnNoWhitespacePassword() { + // For the common "wrong password" case (no whitespace), the legacy tip + // would be misleading noise — it must be suppressed while still surfacing + // the underlying load failure. + java.util.List keystores = + java.util.Collections.singletonList(keystoreFileName); + ListAppender appender = attachAppender(); + try { + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore( + keystores, "wrongnospaces", null)); + assertEquals(TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + String logs = renderLogs(appender); + assertTrue("Witness load failure must still be logged, got: " + logs, + logs.contains("Witness node start failed")); + assertFalse("Legacy-truncation tip must NOT fire for whitespace-free" + + " password, got: " + logs, + logs.contains("first whitespace-separated word")); + } finally { + detachAppender(appender); + } + } + + @Test + public void testTamperedKeystoreRejectedAtSrLoading() throws Exception { + // Address-spoofing defense: a keystore whose declared `address` field does + // not match the address derived from the decrypted private key must be + // rejected at decryption time. Without this check, an attacker who could + // place a file in the SR's keystore dir could trick a witness into signing + // with a different key while displaying a familiar address. The check + // lives in Wallet.decrypt; this test verifies it propagates correctly + // through the WitnessInitializer path. + File dir = new File(System.getProperty("user.dir"), DIR_NAME); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String pwd = "tamperpwd123"; + String generatedName = WalletUtils.generateWalletFile(pwd, keyPair, dir, true); + File keystoreFile = new File(dir, generatedName); + try { + // Tamper: rewrite the address field to a different value than what the + // encrypted private key actually derives to. + ObjectMapper mapper = new ObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + WalletFile wf = mapper.readValue(keystoreFile, WalletFile.class); + String spoofedAddress = "TSpoofedSrAddressXXXXXXXXXXXXXXXXXXX"; + wf.setAddress(spoofedAddress); + mapper.writeValue(keystoreFile, wf); + + java.util.List keystores = + java.util.Collections.singletonList(DIR_NAME + "/" + generatedName); + TronError err = assertThrows(TronError.class, () -> + WitnessInitializer.initFromKeystore(keystores, pwd, null)); + assertEquals("Should be a witness keystore load failure", + TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + Throwable cause = err.getCause(); + assertNotNull("TronError must wrap the underlying CipherException", cause); + assertNotNull("Cause message must not be null", cause.getMessage()); + assertTrue("Cause must mention address mismatch, got: " + cause.getMessage(), + cause.getMessage().contains("address mismatch")); + } finally { + keystoreFile.delete(); + } + } + + private static ListAppender attachAppender() { + ListAppender appender = new ListAppender<>(); + appender.start(); + Logger logger = (Logger) LoggerFactory.getLogger(WitnessInitializer.class); + logger.addAppender(appender); + return appender; + } + + private static void detachAppender(ListAppender appender) { + Logger logger = (Logger) LoggerFactory.getLogger(WitnessInitializer.class); + logger.detachAppender(appender); + appender.stop(); + } + + private static String renderLogs(ListAppender appender) { + StringBuilder sb = new StringBuilder(); + for (ILoggingEvent event : appender.list) { + sb.append(event.getFormattedMessage()).append('\n'); + } + return sb.toString(); + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java index 3ecef5b10c9..e0aa2606473 100644 --- a/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java +++ b/framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -106,7 +107,7 @@ public void testInitFromKeystore() { byte[] keyBytes = Hex.decode(privateKey); when(signInterface.getPrivateKey()).thenReturn(keyBytes); mockedWallet.when(() -> WalletUtils.loadCredentials( - anyString(), any(File.class))).thenReturn(credentials); + anyString(), any(File.class), anyBoolean())).thenReturn(credentials); mockedByteArray.when(() -> ByteArray.toHexString(any())) .thenReturn(privateKey); mockedByteArray.when(() -> ByteArray.fromHexString(anyString())) diff --git a/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java index 88734945687..41fdc2e3925 100644 --- a/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountAssetStoreTest.java @@ -84,11 +84,7 @@ private long createAsset(String tokenName) { AssetIssueCapsule assetIssueCapsule = new AssetIssueCapsule(assetIssueContract); chainBaseManager.getAssetIssueV2Store() .put(assetIssueCapsule.createDbV2Key(), assetIssueCapsule); - try { - ownerCapsule.addAssetV2(ByteArray.fromString(String.valueOf(id)), TOTAL_SUPPLY); - } catch (Exception e) { - e.printStackTrace(); - } + ownerCapsule.addAssetV2(ByteArray.fromString(String.valueOf(id)), TOTAL_SUPPLY); accountStore.put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); return id; } 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 a908d5d3cea..2fae33870cb 100755 --- a/framework/src/test/java/org/tron/core/db/AccountStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountStoreTest.java @@ -77,12 +77,9 @@ public void setAccountTest() throws Exception { field.set(AccountStore.class, new HashMap<>()); Config config = mock(Config.class); Mockito.when(config.getObjectList("genesis.block.assets")).thenReturn(new ArrayList<>()); - try { - AccountStore.setAccount(config); - Assert.fail(); - } catch (Throwable e) { - Assert.assertTrue(e instanceof TronError); - } + Throwable e = Assert.assertThrows(Throwable.class, + () -> AccountStore.setAccount(config)); + Assert.assertTrue(e instanceof TronError); } @Test diff --git a/framework/src/test/java/org/tron/core/db/BlockStoreTest.java b/framework/src/test/java/org/tron/core/db/BlockStoreTest.java index 1868eae4cba..b85a6312278 100644 --- a/framework/src/test/java/org/tron/core/db/BlockStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/BlockStoreTest.java @@ -10,8 +10,6 @@ import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; -import org.tron.core.exception.BadItemException; -import org.tron.core.exception.ItemNotFoundException; @Slf4j @@ -35,56 +33,43 @@ public void testCreateBlockStore() { } @Test - public void testPut() { + public void testPut() throws Exception { long number = 1; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - BlockCapsule blockCapsule1 = blockStore.get(blockId); - Assert.assertNotNull(blockCapsule1); - Assert.assertEquals(number, blockCapsule1.getNum()); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertNotNull(blockCapsule1); + Assert.assertEquals(number, blockCapsule1.getNum()); } @Test - public void testGet() { + public void testGet() throws Exception { long number = 2; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - boolean has = blockStore.has(blockId); - Assert.assertTrue(has); - BlockCapsule blockCapsule1 = blockStore.get(blockId); - - Assert.assertEquals(number, blockCapsule1.getNum()); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + boolean has = blockStore.has(blockId); + Assert.assertTrue(has); + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertEquals(number, blockCapsule1.getNum()); } @Test - public void testDelete() { + public void testDelete() throws Exception { long number = 1; BlockCapsule blockCapsule = getBlockCapsule(number); byte[] blockId = blockCapsule.getBlockId().getBytes(); blockStore.put(blockId, blockCapsule); - try { - BlockCapsule blockCapsule1 = blockStore.get(blockId); - Assert.assertNotNull(blockCapsule1); - Assert.assertEquals(number, blockCapsule1.getNum()); + BlockCapsule blockCapsule1 = blockStore.get(blockId); + Assert.assertNotNull(blockCapsule1); + Assert.assertEquals(number, blockCapsule1.getNum()); - blockStore.delete(blockId); - BlockCapsule blockCapsule2 = blockStore.getUnchecked(blockId); - Assert.assertNull(blockCapsule2); - } catch (ItemNotFoundException | BadItemException e) { - e.printStackTrace(); - } + blockStore.delete(blockId); + BlockCapsule blockCapsule2 = blockStore.getUnchecked(blockId); + Assert.assertNull(blockCapsule2); } } diff --git a/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java b/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java new file mode 100644 index 00000000000..bdb13376f34 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/CheckPointV2StoreTest.java @@ -0,0 +1,161 @@ +package org.tron.core.db; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.lang.reflect.Field; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.RocksDB; +import org.tron.common.TestConstants; +import org.tron.common.storage.WriteOptionsWrapper; +import org.tron.core.config.args.Args; +import org.tron.core.db.common.DbSourceInter; +import org.tron.core.store.CheckPointV2Store; + +public class CheckPointV2StoreTest { + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + static { + RocksDB.loadLibrary(); + } + + @BeforeClass + public static void initArgs() throws IOException { + Args.setParam( + new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF + ); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + + @Test + public void testStubMethods() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-stubs"); + try { + byte[] key = "key".getBytes(); + + store.put(key, new byte[]{}); + Assert.assertNull(store.get(key)); + Assert.assertFalse(store.has(key)); + store.forEach(item -> { + }); + Assert.assertNull(store.spliterator()); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = + (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + store.delete(key); + dbSourceField.set(store, originalDbSource); + + java.lang.reflect.Method initMethod = + CheckPointV2Store.class.getDeclaredMethod("init"); + initMethod.setAccessible(true); + initMethod.invoke(store); + } finally { + store.close(); + } + } + + @Test + public void testCloseWithRealResources() { + CheckPointV2Store store = new CheckPointV2Store("test-close-real"); + // Exercises the real writeOptions.close() and dbSource.closeDB() code paths + store.close(); + } + + @Test + public void testCloseReleasesAllResources() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close"); + + // Replace dbSource with a mock so we can verify closeDB() + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } + + @Test + public void testCloseWhenDbSourceThrows() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close-dbsource-throws"); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + doThrow(new RuntimeException("simulated dbSource failure")).when(mockDbSource).closeDB(); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + } finally { + originalDbSource.closeDB(); + } + } + + @Test + public void testCloseDbSourceWhenWriteOptionsThrows() throws Exception { + CheckPointV2Store store = new CheckPointV2Store("test-close-exception"); + + // Replace child writeOptions with a spy that throws on close + Field childWriteOptionsField = CheckPointV2Store.class.getDeclaredField("writeOptions"); + childWriteOptionsField.setAccessible(true); + WriteOptionsWrapper childWriteOptions = + (WriteOptionsWrapper) childWriteOptionsField.get(store); + WriteOptionsWrapper spyChildWriteOptions = spy(childWriteOptions); + doThrow(new RuntimeException("simulated writeOptions failure")) + .when(spyChildWriteOptions).close(); + childWriteOptionsField.set(store, spyChildWriteOptions); + + // Replace parent writeOptions with a spy that throws on close + Field parentWriteOptionsField = TronDatabase.class.getDeclaredField("writeOptions"); + parentWriteOptionsField.setAccessible(true); + WriteOptionsWrapper parentWriteOptions = + (WriteOptionsWrapper) parentWriteOptionsField.get(store); + WriteOptionsWrapper spyParentWriteOptions = spy(parentWriteOptions); + doThrow(new RuntimeException("simulated parent writeOptions failure")) + .when(spyParentWriteOptions).close(); + parentWriteOptionsField.set(store, spyParentWriteOptions); + + // Replace dbSource with a mock + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(store); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(store, mockDbSource); + + try { + store.close(); + + // dbSource.closeDB() must be called even though both writeOptions threw + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db/DBIteratorTest.java b/framework/src/test/java/org/tron/core/db/DBIteratorTest.java index 58923ce50b6..0966d904093 100644 --- a/framework/src/test/java/org/tron/core/db/DBIteratorTest.java +++ b/framework/src/test/java/org/tron/core/db/DBIteratorTest.java @@ -86,43 +86,36 @@ public void testRocksDb() throws RocksDBException, IOException { RocksDB db = RocksDB.open(options, file.toString())) { db.put("1".getBytes(StandardCharsets.UTF_8), "1".getBytes(StandardCharsets.UTF_8)); db.put("2".getBytes(StandardCharsets.UTF_8), "2".getBytes(StandardCharsets.UTF_8)); - RockStoreIterator iterator = new RockStoreIterator(db.newIterator(), new ReadOptions()); - iterator.seekToFirst(); - Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.getKey()); - Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.next().getValue()); - Assert.assertTrue(iterator.hasNext()); - - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.next().getKey()); - Assert.assertFalse(iterator.hasNext()); - - try { - iterator.seekToLast(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); + try (RockStoreIterator iterator = + new RockStoreIterator(db.newIterator(), new ReadOptions())) { + iterator.seekToFirst(); + Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.getKey()); + Assert.assertArrayEquals("1".getBytes(StandardCharsets.UTF_8), iterator.next().getValue()); + Assert.assertTrue(iterator.hasNext()); + + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.next().getKey()); + Assert.assertFalse(iterator.hasNext()); + + Assert.assertThrows(IllegalStateException.class, iterator::seekToLast); } - iterator = new RockStoreIterator(db.newIterator(), new ReadOptions()); - iterator.seekToLast(); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getKey()); - Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); - iterator.seekToFirst(); - while (iterator.hasNext()) { + try ( + RockStoreIterator iterator = + new RockStoreIterator(db.newIterator(), new ReadOptions())) { + iterator.seekToLast(); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getKey()); + Assert.assertArrayEquals("2".getBytes(StandardCharsets.UTF_8), iterator.getValue()); + iterator.seekToFirst(); + while (iterator.hasNext()) { + iterator.next(); + } + Assert.assertFalse(iterator.hasNext()); + Assert.assertThrows(IllegalStateException.class, iterator::getKey); + Assert.assertThrows(IllegalStateException.class, iterator::getValue); + thrown.expect(NoSuchElementException.class); iterator.next(); } - Assert.assertFalse(iterator.hasNext()); - try { - iterator.getKey(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); - } - try { - iterator.getValue(); - } catch (Exception e) { - Assert.assertTrue(e instanceof IllegalStateException); - } - thrown.expect(NoSuchElementException.class); - iterator.next(); } } 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 b9808b89193..a07fb291f34 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -272,12 +272,8 @@ public void pushBlock() { } } - try { - chainManager.getBlockIdByNum(-1); - Assert.fail(); - } catch (ItemNotFoundException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(ItemNotFoundException.class, + () -> chainManager.getBlockIdByNum(-1)); try { dbManager.getBlockChainHashesOnFork(blockCapsule2.getBlockId()); } catch (Exception e) { @@ -1185,14 +1181,8 @@ public void testExpireTransaction() { TransactionCapsule trx = new TransactionCapsule(tc, ContractType.TransferContract); long latestBlockTime = dbManager.getDynamicPropertiesStore().getLatestBlockHeaderTimestamp(); trx.setExpiration(latestBlockTime - 100); - try { - dbManager.validateCommon(trx); - Assert.fail(); - } catch (TransactionExpirationException e) { - Assert.assertTrue(true); - } catch (TooBigTransactionException e) { - Assert.fail(); - } + Assert.assertThrows(TransactionExpirationException.class, + () -> dbManager.validateCommon(trx)); } @Test diff --git a/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java b/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java index 52adf8e49fa..6bf479c287f 100644 --- a/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java +++ b/framework/src/test/java/org/tron/core/db/TronDatabaseTest.java @@ -1,7 +1,13 @@ package org.tron.core.db; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; +import java.lang.reflect.Field; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -12,7 +18,9 @@ import org.junit.rules.TemporaryFolder; import org.rocksdb.RocksDB; import org.tron.common.TestConstants; +import org.tron.common.storage.WriteOptionsWrapper; import org.tron.core.config.args.Args; +import org.tron.core.db.common.DbSourceInter; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; @@ -99,4 +107,48 @@ public void TestGetFromRoot() throws Assert.assertEquals(db.getFromRoot("test".getBytes()), "test"); } + + @Test + public void testDoCloseDbSourceCalledWhenWriteOptionsThrows() throws Exception { + TronDatabase db = new TronDatabase("test-do-close") { + + @Override + public void put(byte[] key, String item) { + } + + @Override + public void delete(byte[] key) { + } + + @Override + public String get(byte[] key) { + return null; + } + + @Override + public boolean has(byte[] key) { + return false; + } + }; + + Field writeOptionsField = TronDatabase.class.getDeclaredField("writeOptions"); + writeOptionsField.setAccessible(true); + WriteOptionsWrapper spyWriteOptions = spy((WriteOptionsWrapper) writeOptionsField.get(db)); + doThrow(new RuntimeException("simulated writeOptions failure")).when(spyWriteOptions).close(); + writeOptionsField.set(db, spyWriteOptions); + + Field dbSourceField = TronDatabase.class.getDeclaredField("dbSource"); + dbSourceField.setAccessible(true); + DbSourceInter originalDbSource = (DbSourceInter) dbSourceField.get(db); + DbSourceInter mockDbSource = mock(DbSourceInter.class); + dbSourceField.set(db, mockDbSource); + + try { + db.doClose(); + verify(spyWriteOptions).close(); + verify(mockDbSource).closeDB(); + } finally { + originalDbSource.closeDB(); + } + } } diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java index 76e9a18e31a..3ee61065d1f 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java @@ -38,7 +38,7 @@ protected void beforeDestroy() { * from: get key1 or key2, traverse 0 times */ @Test - public void testMergeRoot() { + public void testMergeRoot() throws Exception { // linklist is: from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); //root.setOptimized(true); @@ -68,7 +68,7 @@ public void testMergeRoot() { * */ @Test - public void testMergeAhead() { + public void testMergeAhead() throws Exception { // linklist is: from2 -> from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); @@ -136,7 +136,7 @@ public void testMergeAhead() { * from2: key1=>value1, key2=>value2, key3=>value32, key4=>value4 */ @Test - public void testMergeOverride() { + public void testMergeOverride() throws Exception { // linklist is: from2 -> from -> root SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); SnapshotImpl from = getSnapshotImplIns(root); @@ -165,16 +165,11 @@ public void testMergeOverride() { * The constructor of SnapshotImpl is not public * so reflection is used to construct the object here. */ - private SnapshotImpl getSnapshotImplIns(Snapshot snapshot) { + private SnapshotImpl getSnapshotImplIns(Snapshot snapshot) throws Exception { Class clazz = SnapshotImpl.class; - try { - Constructor constructor = clazz.getDeclaredConstructor(Snapshot.class); - constructor.setAccessible(true); - return (SnapshotImpl) constructor.newInstance(snapshot); - } catch (Exception e) { - e.printStackTrace(); - } - return null; + Constructor constructor = clazz.getDeclaredConstructor(Snapshot.class); + constructor.setAccessible(true); + return (SnapshotImpl) constructor.newInstance(snapshot); } } diff --git a/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java b/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java index 82c887fad53..6c0b8733a18 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventCacheTest.java @@ -20,21 +20,15 @@ public void test() throws Exception { be1.setBlockId(b1); be1.setParentId(b1); be1.setSolidId(b1); - try { - BlockEventCache.add(be1); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof EventException); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> BlockEventCache.add(be1)); + Assert.assertTrue(e1 instanceof EventException); BlockEventCache.init(new BlockCapsule.BlockId(getBlockId(), 100)); - try { - BlockEventCache.add(be1); - Assert.fail(); - } catch (Exception e) { - Assert.assertTrue(e instanceof EventException); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> BlockEventCache.add(be1)); + Assert.assertTrue(e2 instanceof EventException); BlockEventCache.init(b1); diff --git a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java index d1fb95f2f69..e2815e46063 100644 --- a/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java +++ b/framework/src/test/java/org/tron/core/event/BlockEventGetTest.java @@ -34,7 +34,6 @@ import org.tron.core.ChainBaseManager; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; -import org.tron.core.capsule.TransactionRetCapsule; import org.tron.core.capsule.WitnessCapsule; import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @@ -45,7 +44,7 @@ import org.tron.core.services.event.BlockEventGet; import org.tron.core.services.event.bo.BlockEvent; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.core.store.TransactionRetStore; +import org.tron.core.vm.config.ConfigLoader; import org.tron.protos.Protocol; @Slf4j @@ -69,8 +68,8 @@ public class BlockEventGetTest extends BlockGenerate { static LocalDateTime localDateTime = LocalDateTime.now(); - private long time = ZonedDateTime.of(localDateTime, - ZoneId.systemDefault()).toInstant().toEpochMilli(); + private long time = + ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).toInstant().toEpochMilli(); public static String dbPath() { @@ -90,6 +89,7 @@ public static void init() { @Before public void before() throws IOException { + EventPluginLoader.getInstance().setFilterQuery(null); Args.getInstance().setNodeListenPort(10000 + port.incrementAndGet()); dbManager = context.getBean(Manager.class); @@ -99,18 +99,24 @@ public void before() throws IOException { chainManager = dbManager.getChainBaseManager(); tronNetDelegate = context.getBean(TronNetDelegate.class); tronNetDelegate.setExit(false); - currentHeader = dbManager.getDynamicPropertiesStore() - .getLatestBlockHeaderNumberFromDB(); + currentHeader = dbManager.getDynamicPropertiesStore().getLatestBlockHeaderNumberFromDB(); ByteString addressBS = ByteString.copyFrom(address); WitnessCapsule witnessCapsule = new WitnessCapsule(addressBS); chainManager.getWitnessStore().put(address, witnessCapsule); chainManager.addWitness(addressBS); - AccountCapsule accountCapsule = new AccountCapsule(Protocol.Account.newBuilder() - .setAddress(addressBS).setBalance((long) 1e10).build()); + AccountCapsule accountCapsule = new AccountCapsule( + Protocol.Account.newBuilder().setAddress(addressBS).setBalance((long) 1e10).build()); chainManager.getAccountStore().put(address, accountCapsule); + // Reset global static flag that other tests may leave as true, which would prevent + // ConfigLoader.load() from updating VMConfig during VMActuator.execute(). + ConfigLoader.disable = false; + // Reset filterQuery so FilterQueryTest's leftover state does not suppress processTrigger + // coverage when tests share the same Gradle forkEvery JVM batch. + EventPluginLoader.getInstance().setFilterQuery(null); + DynamicPropertiesStore dps = dbManager.getDynamicPropertiesStore(); dps.saveAllowTvmTransferTrc10(1); dps.saveAllowTvmConstantinople(1); @@ -129,8 +135,8 @@ public void test() throws Exception { Manager manager = context.getBean(Manager.class); WitnessCapsule witnessCapsule = new WitnessCapsule(ByteString.copyFrom(address)); - ChainBaseManager.getChainBaseManager() - .getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + ChainBaseManager.getChainBaseManager().getWitnessScheduleStore() + .saveActiveWitnesses(new ArrayList<>()); ChainBaseManager.getChainBaseManager().addWitness(ByteString.copyFrom(address)); String code = "608060405234801561000f575f80fd5b50d3801561001b575f80fd5b50d28015610027575f" @@ -141,15 +147,16 @@ public void test() throws Exception { + "00a0565b9050919050565b6100dc816100b2565b82525050565b5f6020820190506100f55f83018461" + "00d3565b92915050565b603e806101075f395ff3fe60806040525f80fdfea26474726f6e582212200c" + "57c973388f044038eff0e6474425b38037e75e66d6b3047647290605449c7764736f6c63430008140033"; - Protocol.Transaction trx = TvmTestUtils.generateDeploySmartContractAndGetTransaction( - "TestTRC20", address, "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\"" - + ":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\"" - + ":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\"" - + ":\"Transfer\",\"type\":\"event\"}]", code, 0, (long) 1e9, 100, null, 1); - trx = trx.toBuilder().addRet( - Protocol.Transaction.Result.newBuilder() - .setContractRetValue(Protocol.Transaction.Result.contractResult.SUCCESS_VALUE) - .build()).build(); + Protocol.Transaction trx = + TvmTestUtils.generateDeploySmartContractAndGetTransaction("TestTRC20", address, + "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\"" + + ":\"from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"to\",\"type\"" + + ":\"address\"},{\"indexed\":false,\"name\":\"value\"," + + "\"type\":\"uint256\"}],\"name\"" + + ":\"Transfer\",\"type\":\"event\"}]", code, 0, (long) 1e9, 100, null, 1); + trx = trx.toBuilder().addRet(Protocol.Transaction.Result.newBuilder() + .setContractRetValue(Protocol.Transaction.Result.contractResult.SUCCESS_VALUE).build()) + .build(); Protocol.Block block = getSignedBlock(witnessCapsule.getAddress(), time, privateKey); BlockCapsule blockCapsule = new BlockCapsule(block.toBuilder().addTransactions(trx).build()); @@ -163,8 +170,7 @@ public void test() throws Exception { // Set energy price history to test boundary cases manager.getDynamicPropertiesStore().saveEnergyPriceHistory( - manager.getDynamicPropertiesStore().getEnergyPriceHistory() - + "," + time + ":210"); + manager.getDynamicPropertiesStore().getEnergyPriceHistory() + "," + time + ":210"); EventPluginConfig config = new EventPluginConfig(); config.setSendQueueLength(1000); @@ -207,8 +213,9 @@ public void test() throws Exception { // Here energy unit price should be 100 not 210, // cause block time is equal to 210`s effective time - Assert.assertEquals(100, blockEvent.getTransactionLogTriggerCapsules() - .get(0).getTransactionLogTrigger().getEnergyUnitPrice()); + Assert.assertEquals(100, + blockEvent.getTransactionLogTriggerCapsules().get(0).getTransactionLogTrigger() + .getEnergyUnitPrice()); } catch (Exception e) { Assert.fail(); } @@ -217,8 +224,8 @@ public void test() throws Exception { @Test public void getTransactionTriggers() throws Exception { BlockEventGet blockEventGet = new BlockEventGet(); - BlockCapsule bc = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - 100, Sha256Hash.ZERO_HASH.getByteString()); + BlockCapsule bc = + new BlockCapsule(1, Sha256Hash.ZERO_HASH, 100, Sha256Hash.ZERO_HASH.getByteString()); List list = blockEventGet.getTransactionTriggers(bc, 1); Assert.assertEquals(0, list.size()); @@ -237,7 +244,7 @@ public void getTransactionTriggers() throws Exception { Manager manager = mock(Manager.class); ReflectUtils.setFieldValue(blockEventGet, "manager", manager); Mockito.when(manager.getTransactionInfoByBlockNum(1)) - .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); + .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); list = blockEventGet.getTransactionTriggers(bc, 1); Assert.assertEquals(1, list.size()); @@ -254,8 +261,7 @@ public void getTransactionTriggers() throws Exception { resourceBuild.setNetUsage(6); String address = "A0B4750E2CD76E19DCA331BF5D089B71C3C2798548"; - infoBuild - .setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(address))) + infoBuild.setContractAddress(ByteString.copyFrom(ByteArray.fromHexString(address))) .addContractResult(ByteString.copyFrom(ByteArray.fromHexString("112233"))) .setReceipt(resourceBuild.build()); @@ -263,14 +269,12 @@ public void getTransactionTriggers() throws Exception { Mockito.when(manager.getChainBaseManager()).thenReturn(chainBaseManager); - GrpcAPI.TransactionInfoList result = GrpcAPI.TransactionInfoList.newBuilder() - .addTransactionInfo(infoBuild.build()).build(); + GrpcAPI.TransactionInfoList result = + GrpcAPI.TransactionInfoList.newBuilder().addTransactionInfo(infoBuild.build()).build(); - Mockito.when(manager.getTransactionInfoByBlockNum(0)) - .thenReturn(result); + Mockito.when(manager.getTransactionInfoByBlockNum(0)).thenReturn(result); - Protocol.Block block = Protocol.Block.newBuilder() - .addTransactions(transaction).build(); + Protocol.Block block = Protocol.Block.newBuilder().addTransactions(transaction).build(); BlockCapsule blockCapsule = new BlockCapsule(block); blockCapsule.getTransactions().forEach(t -> t.setBlockNum(blockCapsule.getNum())); @@ -278,23 +282,17 @@ public void getTransactionTriggers() throws Exception { list = blockEventGet.getTransactionTriggers(blockCapsule, 1); Assert.assertEquals(1, list.size()); - Assert.assertEquals(1, - list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getBlockNumber()); - Assert.assertEquals(2, - list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); + Assert.assertEquals(1, list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getBlockNumber()); + Assert.assertEquals(2, list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); Mockito.when(manager.getTransactionInfoByBlockNum(0)) .thenReturn(GrpcAPI.TransactionInfoList.newBuilder().build()); list = blockEventGet.getTransactionTriggers(blockCapsule, 1); Assert.assertEquals(1, list.size()); - Assert.assertEquals(1, - list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getBlockNumber()); - Assert.assertEquals(0, - list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); + Assert.assertEquals(1, list.get(0).getTransactionLogTrigger().getLatestSolidifiedBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getBlockNumber()); + Assert.assertEquals(0, list.get(0).getTransactionLogTrigger().getEnergyUsageTotal()); } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index e965ae3fd60..344b6dd946d 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -139,8 +139,9 @@ public void shutdownBlockTimeInitTest() { Map params = new HashMap<>(); params.put("node.shutdown.BlockTime", "0"); params.put("storage.db.directory", "database"); - Config config = ConfigFactory.defaultOverrides().withFallback( - ConfigFactory.parseMap(params)); + Config config = ConfigFactory.defaultOverrides() + .withFallback(ConfigFactory.parseMap(params)) + .withFallback(ConfigFactory.defaultReference()); TronError thrown = assertThrows(TronError.class, () -> Args.applyConfigParams(config)); assertEquals(TronError.ErrCode.AUTO_STOP_PARAMS, thrown.getErrCode()); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java b/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java index abb73671161..2cdcaaf7a53 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/ConcurrentHashMapTest.java @@ -7,9 +7,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.logsfilter.capsule.BlockFilterCapsule; import org.tron.common.utils.ByteArray; import org.tron.core.exception.ItemNotFoundException; @@ -18,6 +22,7 @@ @Slf4j public class ConcurrentHashMapTest { + private static final String EXECUTOR_NAME = "jsonrpc-concurrent-map-test"; private static int randomInt(int minInt, int maxInt) { return (int) round(random(true) * (maxInt - minInt) + minInt, true); @@ -52,12 +57,14 @@ public void testHandleBlockHash() { try { Thread.sleep(200); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + Assert.fail("Interrupted during test setup: " + e.getMessage()); } - Thread putThread = new Thread(new Runnable() { - public void run() { + ExecutorService executor = ExecutorServiceManager.newFixedThreadPool(EXECUTOR_NAME, 4, true); + try { + Future putTask = executor.submit(() -> { for (int i = 1; i <= times; i++) { logger.info("put time {}, from {} to {}", i, (1 + (i - 1) * eachCount), i * eachCount); @@ -69,21 +76,20 @@ public void run() { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("putThread interrupted", e); } } - } - - }); + }); - Thread getThread1 = new Thread(new Runnable() { - public void run() { + Future getTask1 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread1 interrupted", e); } logger.info("Thread1 get time {}", t); @@ -98,22 +104,20 @@ public void run() { } } catch (ItemNotFoundException e) { - e.printStackTrace(); - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } - } - }); + }); - Thread getThread2 = new Thread(new Runnable() { - public void run() { + Future getTask2 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread2 interrupted", e); } logger.info("Thread2 get time {}", t); @@ -132,21 +136,20 @@ public void run() { } } catch (ItemNotFoundException e) { - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } - } - }); + }); - Thread getThread3 = new Thread(new Runnable() { - public void run() { + Future getTask3 = executor.submit(() -> { for (int t = 1; t <= times * 2; t++) { try { Thread.sleep(randomInt(50, 100)); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new AssertionError("getThread3 interrupted", e); } logger.info("Thread3 get time {}", t); @@ -160,31 +163,30 @@ public void run() { try { resultMap3.get(String.valueOf(k)).add(str.toString()); } catch (Exception e) { - logger.error("resultMap3 get {} exception {}", k, e.getMessage()); - e.printStackTrace(); + throw new AssertionError("resultMap3 get " + k + " exception", e); } } } catch (ItemNotFoundException e) { - // Assert.fail(e.getMessage()); + Assert.fail("Filter ID should always exist: " + e.getMessage()); } } } + }); + + for (Future future : new Future[] {putTask, getTask1, getTask2, getTask3}) { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Assert.fail("Main thread interrupted while waiting for worker threads: " + + e.getMessage()); + } catch (ExecutionException e) { + Assert.fail("Worker thread failed: " + e.getCause()); + } } - }); - - putThread.start(); - getThread1.start(); - getThread2.start(); - getThread3.start(); - - try { - putThread.join(); - getThread1.join(); - getThread2.join(); - getThread3.join(); - } catch (InterruptedException e) { - e.printStackTrace(); + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(executor, EXECUTOR_NAME); } logger.info("-----------------------------------------------------------------------"); @@ -205,4 +207,4 @@ public void run() { } } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index ced7048c9d2..8c22d525bb5 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -8,6 +8,8 @@ import com.google.gson.JsonObject; import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; +import java.io.ByteArrayInputStream; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -25,6 +28,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.application.HttpService; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.Metrics; import org.tron.common.utils.ByteArray; @@ -345,20 +349,14 @@ public void testGetBlockByNumber() { Assert.assertEquals(ByteArray.toJsonHex(blockCapsule2.getNum()), blockResult.getNumber()); // pending - try { - tronJsonRpc.ethGetBlockByNumber("pending", false); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("pending", false)); + Assert.assertEquals("TAG pending not supported", e1.getMessage()); // invalid - try { - tronJsonRpc.ethGetBlockByNumber("0x", false); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("0x", false)); + Assert.assertEquals("invalid block number", e2.getMessage()); } @Test @@ -432,12 +430,9 @@ public void testServicesInit() { public void testGetByJsonBlockId() { long blkNum = 0; - try { - getByJsonBlockId("pending", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("pending", wallet)); + Assert.assertEquals("TAG pending not supported", pendingEx.getMessage()); try { blkNum = getByJsonBlockId(null, wallet); @@ -467,49 +462,34 @@ public void testGetByJsonBlockId() { } Assert.assertEquals(10L, blkNum); - try { - getByJsonBlockId("abc", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("Incorrect hex syntax", e.getMessage()); - } + Exception abcEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("abc", wallet)); + Assert.assertEquals("Incorrect hex syntax", abcEx.getMessage()); - try { - getByJsonBlockId("0xxabc", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed - Assert.assertTrue(e.getMessage().startsWith("For input string: \"xabc\"")); - } + Exception hexEx = Assert.assertThrows(Exception.class, + () -> getByJsonBlockId("0xxabc", wallet)); + // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed + Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); } @Test public void testGetTrxBalance() { String balance = ""; - try { - tronJsonRpc.getTrxBalance("", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); - try { - tronJsonRpc.getTrxBalance("", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); - try { - tronJsonRpc.getTrxBalance("", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); try { balance = tronJsonRpc.getTrxBalance("0xabd4b9367799eaa3197fecb144eb71de1e049abc", @@ -522,83 +502,56 @@ public void testGetTrxBalance() { @Test public void testGetStorageAt() { - try { - tronJsonRpc.getStorageAt("", "", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getStorageAt("", "", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getStorageAt("", "", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } @Test public void testGetABIOfSmartContract() { - try { - tronJsonRpc.getABIOfSmartContract("", "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getABIOfSmartContract("", "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getABIOfSmartContract("", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } @Test public void testGetCall() { - try { - tronJsonRpc.getCall(null, "earliest"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getCall(null, "pending"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } - - try { - tronJsonRpc.getCall(null, "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "earliest")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "pending")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "finalized")); + Assert.assertEquals("TAG [earliest | pending | finalized] not supported", + e3.getMessage()); } /** @@ -666,13 +619,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x78", "0x14", - null, null, null), 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); - } + JsonRpcInvalidParamsException fromToEx = + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x78", "0x14", + null, null, null), 100, null, false)); + Assert.assertEquals("please verify: fromBlock <= toBlock", fromToEx.getMessage()); //fromBlock or toBlock is not hex num try { @@ -691,13 +642,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("pending", null, null, null, null), - 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + JsonRpcInvalidParamsException pendingFilterEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("pending", null, null, null, null), + 100, null, false)); + Assert.assertEquals("TAG pending not supported", pendingFilterEx.getMessage()); try { LogFilterWrapper logFilterWrapper = new LogFilterWrapper(new FilterRequest("finalized", null, null, null, null), 100, wallet, false); @@ -706,13 +655,11 @@ public void testLogFilterWrapper() { } catch (JsonRpcInvalidParamsException e) { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("test", null, null, null, null), - 100, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals("Incorrect hex syntax", e.getMessage()); - } + JsonRpcInvalidParamsException testSyntaxEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("test", null, null, null, null), + 100, null, false)); + Assert.assertEquals("Incorrect hex syntax", testSyntaxEx.getMessage()); // to = 8000 try { @@ -722,15 +669,13 @@ public void testLogFilterWrapper() { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx1 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x0", "0x1f40", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx1.getMessage()); try { new LogFilterWrapper(new FilterRequest("0x0", "latest", null, @@ -739,15 +684,13 @@ public void testLogFilterWrapper() { Assert.fail(); } - try { - new LogFilterWrapper(new FilterRequest("0x0", "latest", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx2 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x0", "latest", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx2.getMessage()); // from = 100, current = 5_000, to = Long.MAX_VALUE try { @@ -764,15 +707,13 @@ public void testLogFilterWrapper() { } // from = 100 - try { - new LogFilterWrapper(new FilterRequest("0x64", "latest", null, - null, null), LATEST_BLOCK_NUM, null, true); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, - e.getMessage()); - } + JsonRpcInvalidParamsException rangeEx3 = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0x64", "latest", null, + null, null), LATEST_BLOCK_NUM, null, true)); + Assert.assertEquals( + "exceed max block range: " + Args.getInstance().jsonRpcMaxBlockRange, + rangeEx3.getMessage()); try { new LogFilterWrapper(new FilterRequest("0x64", "latest", null, null, null), LATEST_BLOCK_NUM, null, false); @@ -866,15 +807,13 @@ public void testMaxSubTopics() { } topics.add(subTopics); - try { - new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", - null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false); - Assert.fail("Expected to be thrown"); - } catch (JsonRpcInvalidParamsException e) { - Assert.assertEquals( - "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), - e.getMessage()); - } + JsonRpcInvalidParamsException topicsEx = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> new LogFilterWrapper(new FilterRequest("0xbb8", "0x1f40", + null, topics.toArray(), null), LATEST_BLOCK_NUM, null, false)); + Assert.assertEquals( + "exceed max topics: " + Args.getInstance().getJsonRpcMaxSubTopics(), + topicsEx.getMessage()); try { tronJsonRpc.getLogs(new FilterRequest("0xbb8", "0x1f40", @@ -978,40 +917,25 @@ public void testNewFilterFinalizedBlock() { Assert.fail(); } - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", null, null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", null, null, null, null))); + Assert.assertEquals("invalid block range params", e1.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest(null, "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest(null, "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e2.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", "latest", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "latest", null, null, null))); + Assert.assertEquals("invalid block range params", e3.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e4.getMessage()); - try { - tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); - } + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e5.getMessage()); } @Test @@ -1052,19 +976,13 @@ public void testGetBlockReceipts() { throw new RuntimeException(e); } - try { - tronJsonRpc.getBlockReceipts("pending"); - Assert.fail(); - } catch (Exception e) { - Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, e.getMessage()); - } + Exception pendingReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("pending")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingReceiptsEx.getMessage()); - try { - tronJsonRpc.getBlockReceipts("test"); - Assert.fail(); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception testReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("test")); + Assert.assertEquals("invalid block number", testReceiptsEx.getMessage()); try { List transactionReceiptList = tronJsonRpc.getBlockReceipts("0x2"); @@ -1099,4 +1017,71 @@ public void testWeb3ClientVersion() { Assert.fail(); } } + + /** + * Verifies SizeLimitHandler integration with the real JsonRpcServlet + jsonrpc4j stack. + * + * Covers: normal request no regression, Content-Length oversized 413, + * and chunked oversized handled gracefully (body truncated, 200 + empty body + * because jsonrpc4j absorbs the BadMessageException). + */ + @Test + public void testJsonRpcSizeLimitIntegration() { + long testLimit = 1024; + long originalLimit = fullNodeJsonRpcHttpService.getMaxRequestSize(); + try { + fullNodeJsonRpcHttpService.setMaxRequestSize(testLimit); + + fullNodeJsonRpcHttpService.start(); + String url = "http://127.0.0.1:" + + CommonParameter.getInstance().getJsonRpcHttpFullNodePort() + "/jsonrpc"; + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Normal JSON-RPC request passes through SizeLimitHandler + JsonObject req = new JsonObject(); + req.addProperty("jsonrpc", "2.0"); + req.addProperty("method", "web3_clientVersion"); + req.addProperty("id", 1); + + HttpPost post = new HttpPost(url); + post.addHeader("Content-Type", "application/json"); + post.setEntity(new StringEntity(req.toString())); + CloseableHttpResponse resp = httpClient.execute(post); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + String body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Normal JSON-RPC response should contain result", + body.contains("result")); + resp.close(); + + // Oversized request with Content-Length -> 413 before JsonRpcServlet + HttpPost overPost = new HttpPost(url); + overPost.addHeader("Content-Type", "application/json"); + overPost.setEntity(new StringEntity( + new String(new char[(int) testLimit + 1]).replace('\0', 'x'))); + resp = httpClient.execute(overPost); + Assert.assertEquals(413, resp.getStatusLine().getStatusCode()); + resp.close(); + + // Chunked oversized -> BadMessageException thrown during body read, + // absorbed by jsonrpc4j catch(Exception) -> 200 with empty body. + // Body read IS truncated at the limit - OOM protection effective. + byte[] chunkedData = new String(new char[(int) testLimit * 2]) + .replace('\0', 'x').getBytes("UTF-8"); + HttpPost chunkedPost = new HttpPost(url); + chunkedPost.setEntity(new InputStreamEntity( + new ByteArrayInputStream(chunkedData), -1)); + resp = httpClient.execute(chunkedPost); + Assert.assertEquals(200, resp.getStatusLine().getStatusCode()); + body = EntityUtils.toString(resp.getEntity()); + Assert.assertTrue("Chunked oversized should return empty body" + + " (jsonrpc4j absorbs BadMessageException)", body.isEmpty()); + resp.close(); + } + } catch (Exception e) { + Assert.fail(e.getMessage()); + } finally { + fullNodeJsonRpcHttpService.setMaxRequestSize(originalLimit); + fullNodeJsonRpcHttpService.stop(); + } + } } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java index d80d10694a8..94269e86fec 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogBlockQueryTest.java @@ -3,7 +3,6 @@ import java.lang.reflect.Method; import java.util.BitSet; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Resource; import org.junit.After; import org.junit.Assert; @@ -11,6 +10,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.es.ExecutorServiceManager; import org.tron.core.config.args.Args; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; import org.tron.core.services.jsonrpc.filters.LogBlockQuery; @@ -21,6 +21,7 @@ public class LogBlockQueryTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + private static final String EXECUTOR_NAME = "log-block-query-test"; private ExecutorService sectionExecutor; private Method partialMatchMethod; private static final long CURRENT_MAX_BLOCK_NUM = 50000L; @@ -31,10 +32,10 @@ public class LogBlockQueryTest extends BaseTest { @Before public void setup() throws Exception { - sectionExecutor = Executors.newFixedThreadPool(5); - + sectionExecutor = ExecutorServiceManager.newFixedThreadPool(EXECUTOR_NAME, 5); + // Get private method through reflection - partialMatchMethod = LogBlockQuery.class.getDeclaredMethod("partialMatch", + partialMatchMethod = LogBlockQuery.class.getDeclaredMethod("partialMatch", int[][].class, int.class); partialMatchMethod.setAccessible(true); @@ -52,9 +53,7 @@ public void setup() throws Exception { @After public void tearDown() { - if (sectionExecutor != null && !sectionExecutor.isShutdown()) { - sectionExecutor.shutdown(); - } + ExecutorServiceManager.shutdownAndAwaitTermination(sectionExecutor, EXECUTOR_NAME); } @Test @@ -63,8 +62,8 @@ public void testPartialMatch() throws Exception { LogFilterWrapper logFilterWrapper = new LogFilterWrapper( new FilterRequest("0x0", "0x1", null, null, null), CURRENT_MAX_BLOCK_NUM, null, false); - - LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, + + LogBlockQuery logBlockQuery = new LogBlockQuery(logFilterWrapper, sectionBloomStore, CURRENT_MAX_BLOCK_NUM, sectionExecutor); int section = 0; @@ -105,4 +104,4 @@ public void testPartialMatch() throws Exception { Assert.assertNotNull(result); Assert.assertTrue(result.isEmpty()); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java index d31f7a4f63d..39bcc30e278 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/SectionBloomStoreTest.java @@ -4,12 +4,14 @@ import java.util.BitSet; import java.util.List; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Resource; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.ByteArray; @@ -28,10 +30,22 @@ public class SectionBloomStoreTest extends BaseTest { @Resource SectionBloomStore sectionBloomStore; + private ExecutorService sectionExecutor; + static { Args.setParam(new String[] {"--output-directory", dbPath()}, TestConstants.TEST_CONF); } + @Before + public void setUp() { + sectionExecutor = ExecutorServiceManager.newFixedThreadPool("section-bloom-query", 5); + } + + @After + public void tearDown() { + ExecutorServiceManager.shutdownAndAwaitTermination(sectionExecutor, "section-bloom-query"); + } + @Test public void testPutAndGet() { BitSet bitSet = new BitSet(SectionBloomStore.BLOCK_PER_SECTION); @@ -126,7 +140,6 @@ public void testWriteAndQuery() { } long currentMaxBlockNum = 50000; - ExecutorService sectionExecutor = Executors.newFixedThreadPool(5); //query one address try { @@ -236,6 +249,5 @@ public void testWriteAndQuery() { Assert.fail(); } - sectionExecutor.shutdownNow(); } } diff --git a/framework/src/test/java/org/tron/core/net/MessageTest.java b/framework/src/test/java/org/tron/core/net/MessageTest.java index 5b81d18a599..3757333aa6d 100644 --- a/framework/src/test/java/org/tron/core/net/MessageTest.java +++ b/framework/src/test/java/org/tron/core/net/MessageTest.java @@ -19,29 +19,16 @@ public class MessageTest { private DisconnectMessage disconnectMessage; @Test - public void test1() throws Exception { - byte[] bytes = new DisconnectMessage(ReasonCode.TOO_MANY_PEERS).getData(); + public void test1() { DisconnectMessageTest disconnectMessageTest = new DisconnectMessageTest(); try { disconnectMessage = new DisconnectMessage(MessageTypes.P2P_DISCONNECT.asByte(), disconnectMessageTest.toByteArray()); } catch (Exception e) { - System.out.println(e.getMessage()); Assert.assertTrue(e instanceof P2pException); } } - public void test2() throws Exception { - DisconnectMessageTest disconnectMessageTest = new DisconnectMessageTest(); - long startTime = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - disconnectMessage = new DisconnectMessage(MessageTypes.P2P_DISCONNECT.asByte(), - disconnectMessageTest.toByteArray()); - } - long endTime = System.currentTimeMillis(); - System.out.println("spend time : " + (endTime - startTime)); - } - @Test public void testMessageStatistics() { MessageStatistics messageStatistics = new MessageStatistics(); diff --git a/framework/src/test/java/org/tron/core/net/NodeTest.java b/framework/src/test/java/org/tron/core/net/NodeTest.java index cbf545af646..979c306fd98 100644 --- a/framework/src/test/java/org/tron/core/net/NodeTest.java +++ b/framework/src/test/java/org/tron/core/net/NodeTest.java @@ -28,30 +28,18 @@ public class NodeTest { public void testIpV4() { InetSocketAddress address1 = NetUtil.parseInetSocketAddress("192.168.0.1:18888"); Assert.assertNotNull(address1); - try { - NetUtil.parseInetSocketAddress("192.168.0.1"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("192.168.0.1")); } @Test public void testIpV6() { - try { - NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb:18888"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb:18888")); InetSocketAddress address2 = NetUtil.parseInetSocketAddress("[fe80::216:3eff:fe0e:23bb]:18888"); Assert.assertNotNull(address2); - try { - NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb"); - Assert.fail(); - } catch (RuntimeException e) { - Assert.assertTrue(true); - } + Assert.assertThrows(RuntimeException.class, + () -> NetUtil.parseInetSocketAddress("fe80::216:3eff:fe0e:23bb")); } @Test diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java index 270002fffba..e05ee29d015 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java @@ -111,21 +111,17 @@ public void testSyncFetchCheck() { FetchInvDataMsgHandler fetchInvDataMsgHandler = new FetchInvDataMsgHandler(); - try { - Mockito.when(peer.getLastSyncBlockId()) + Mockito.when(peer.getLastSyncBlockId()) .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 1000L)); - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals(e.getMessage(), "maxBlockNum: 1000, blockNum: 10000"); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("maxBlockNum: 1000, blockNum: 10000", e1.getMessage()); - try { - Mockito.when(peer.getLastSyncBlockId()) + Mockito.when(peer.getLastSyncBlockId()) .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 20000L)); - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals(e.getMessage(), "minBlockNum: 16000, blockNum: 10000"); - } + Exception e2 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("minBlockNum: 16000, blockNum: 10000", e2.getMessage()); } @Test @@ -148,15 +144,12 @@ public void testRateLimiter() { Mockito.when(peer.getP2pRateLimiter()).thenReturn(p2pRateLimiter); FetchInvDataMsgHandler fetchInvDataMsgHandler = new FetchInvDataMsgHandler(); - try { - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertEquals("fetch too many blocks, size:101", e.getMessage()); - } - try { - fetchInvDataMsgHandler.processMessage(peer, msg); - } catch (Exception e) { - Assert.assertTrue(e.getMessage().endsWith("rate limit")); - } + Exception e1 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertEquals("fetch too many blocks, size:101", e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> fetchInvDataMsgHandler.processMessage(peer, msg)); + Assert.assertTrue(e2.getMessage().endsWith("rate limit")); } } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java index 338b44e6699..1dbf7c7150f 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java @@ -49,6 +49,7 @@ public void testProcessMessage() throws Exception { field.set(handler, tronNetDelegate); handler.processMessage(peer, msg); + Mockito.verify(tronNetDelegate, Mockito.atLeastOnce()).isBlockUnsolidified(); } private Channel getChannel(String host, int port) throws Exception { diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java index b1fb197a2e9..be843674632 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/MessageHandlerTest.java @@ -13,13 +13,13 @@ import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.Sha256Hash; import org.tron.consensus.pbft.message.PbftMessage; import org.tron.core.capsule.BlockCapsule; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.net.P2pEventHandlerImpl; import org.tron.core.net.TronNetService; @@ -33,6 +33,8 @@ public class MessageHandlerTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private PeerConnection peer; private static P2pEventHandlerImpl p2pEventHandler; private static ApplicationContext ctx; @@ -45,7 +47,7 @@ public class MessageHandlerTest { public static void init() throws Exception { Args.setParam(new String[] {"--output-directory", temporaryFolder.newFolder().toString(), "--debug"}, TestConstants.TEST_CONF); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); p2pEventHandler = context.getBean(P2pEventHandlerImpl.class); ctx = (ApplicationContext) ReflectUtils.getFieldObject(p2pEventHandler, "ctx"); @@ -57,7 +59,7 @@ public static void init() throws Exception { @AfterClass public static void destroy() { Args.clearParam(); - context.destroy(); + APP_FIXTURE.close(); } @After diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java index 649e5eb0875..cc30fb70b0b 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerConnectionTest.java @@ -12,6 +12,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; +import org.tron.common.TestConstants; import org.tron.common.overlay.message.Message; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Pair; @@ -32,6 +33,7 @@ public class PeerConnectionTest { @BeforeClass public static void initArgs() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); CommonParameter.getInstance().setRateLimiterSyncBlockChain(10); CommonParameter.getInstance().setRateLimiterFetchInvData(10); CommonParameter.getInstance().setRateLimiterDisconnect(10); @@ -203,6 +205,8 @@ public void testSetChannel() { relayNodes.add(inetSocketAddress); peerConnection.setChannel(c1); Assert.assertTrue(peerConnection.isRelayPeer()); + + ReflectUtils.setFieldValue(peerConnection, "relayNodes", new ArrayList<>()); } @Test @@ -234,15 +238,12 @@ public void testCheckAndPutAdvInvRequest() { @Test public void testEquals() { - List relayNodes = new ArrayList<>(); - PeerConnection p1 = new PeerConnection(); InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.2", 10001); Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); p1.setChannel(c1); PeerConnection p2 = new PeerConnection(); @@ -251,7 +252,6 @@ public void testEquals() { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); p2.setChannel(c2); PeerConnection p3 = new PeerConnection(); @@ -260,7 +260,6 @@ public void testEquals() { Channel c3 = new Channel(); ReflectUtils.setFieldValue(c3, "inetSocketAddress", inetSocketAddress3); ReflectUtils.setFieldValue(c3, "inetAddress", inetSocketAddress3.getAddress()); - ReflectUtils.setFieldValue(p3, "relayNodes", relayNodes); p3.setChannel(c3); Assert.assertTrue(p1.equals(p1)); @@ -270,15 +269,12 @@ public void testEquals() { @Test public void testHashCode() { - List relayNodes = new ArrayList<>(); - PeerConnection p1 = new PeerConnection(); InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.2", 10001); Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); p1.setChannel(c1); PeerConnection p2 = new PeerConnection(); @@ -287,7 +283,6 @@ public void testHashCode() { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); p2.setChannel(c2); PeerConnection p3 = new PeerConnection(); @@ -296,7 +291,6 @@ public void testHashCode() { Channel c3 = new Channel(); ReflectUtils.setFieldValue(c3, "inetSocketAddress", inetSocketAddress3); ReflectUtils.setFieldValue(c3, "inetAddress", inetSocketAddress3.getAddress()); - ReflectUtils.setFieldValue(p3, "relayNodes", relayNodes); p3.setChannel(c3); Assert.assertTrue(p1.hashCode() != p2.hashCode()); diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java index ee409a8ab04..ffba127a6fd 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerManagerTest.java @@ -15,16 +15,17 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.context.ApplicationContext; +import org.tron.common.TestConstants; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ReflectUtils; import org.tron.core.config.args.Args; import org.tron.p2p.connection.Channel; public class PeerManagerTest { - List relayNodes = new ArrayList<>(); @BeforeClass public static void initArgs() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); CommonParameter.getInstance().setRateLimiterSyncBlockChain(10); CommonParameter.getInstance().setRateLimiterFetchInvData(10); CommonParameter.getInstance().setRateLimiterDisconnect(10); @@ -54,7 +55,7 @@ public void testAdd() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -79,7 +80,7 @@ public void testRemove() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -105,7 +106,7 @@ public void testGetPeerConnection() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -128,7 +129,7 @@ public void testGetPeers() throws Exception { Channel c1 = new Channel(); ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress1); ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress1.getAddress()); - ReflectUtils.setFieldValue(p1, "relayNodes", relayNodes); + p1.setChannel(c1); ApplicationContext ctx = mock(ApplicationContext.class); @@ -146,7 +147,7 @@ public void testGetPeers() throws Exception { Channel c2 = new Channel(); ReflectUtils.setFieldValue(c2, "inetSocketAddress", inetSocketAddress2); ReflectUtils.setFieldValue(c2, "inetAddress", inetSocketAddress2.getAddress()); - ReflectUtils.setFieldValue(p2, "relayNodes", relayNodes); + p2.setChannel(c2); ApplicationContext ctx2 = mock(ApplicationContext.class); diff --git a/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java b/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java index 80b1abdc35d..d2ee4be5b87 100644 --- a/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java +++ b/framework/src/test/java/org/tron/core/net/peer/PeerStatusCheckMockTest.java @@ -1,11 +1,17 @@ package org.tron.core.net.peer; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; import org.mockito.Mockito; +import org.tron.common.utils.ReflectUtils; public class PeerStatusCheckMockTest { @After @@ -14,13 +20,25 @@ public void clearMocks() { } @Test - public void testInitException() throws InterruptedException { + public void testInitException() { PeerStatusCheck peerStatusCheck = spy(new PeerStatusCheck()); + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + ReflectUtils.setFieldValue(peerStatusCheck, "peerStatusCheckExecutor", executor); doThrow(new RuntimeException("test exception")).when(peerStatusCheck).statusCheck(); + peerStatusCheck.init(); - // the initialDelay of scheduleWithFixedDelay is 5s - Thread.sleep(5000L); + Mockito.verify(executor).scheduleWithFixedDelay(any(Runnable.class), eq(5L), eq(2L), + eq(TimeUnit.SECONDS)); + Runnable scheduledTask = Mockito.mockingDetails(executor).getInvocations().stream() + .filter(invocation -> invocation.getMethod().getName().equals("scheduleWithFixedDelay")) + .map(invocation -> (Runnable) invocation.getArgument(0)) + .findFirst() + .orElseThrow(() -> new AssertionError("scheduled task was not registered")); + + scheduledTask.run(); + + Mockito.verify(peerStatusCheck).statusCheck(); } -} \ No newline at end of file +} 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 6f34288939f..8585244b941 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 @@ -89,7 +89,6 @@ private void initWitness() { // Base58: TNboetpFgv9SqMoHvaVt626NLXETnbdW1K byte[] key = Hex.decode("418A8D690BF36806C36A7DAE3AF796643C1AA9CC01");//exist already WitnessCapsule witnessCapsule = chainBaseManager.getWitnessStore().get(key); - System.out.println(witnessCapsule.getInstance()); witnessCapsule.setVoteCount(1000); chainBaseManager.getWitnessStore().put(key, witnessCapsule); List list = new ArrayList<>(); diff --git a/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java b/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java index a940a14d392..7ad56c464bb 100644 --- a/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java +++ b/framework/src/test/java/org/tron/core/net/services/TronStatsManagerTest.java @@ -7,8 +7,10 @@ import org.junit.Assert; import org.junit.Test; +import org.tron.core.net.TronNetService; import org.tron.core.net.service.statistics.NodeStatistics; import org.tron.core.net.service.statistics.TronStatsManager; +import org.tron.p2p.stats.P2pStats; import org.tron.protos.Protocol; public class TronStatsManagerTest { @@ -50,14 +52,20 @@ public void testWork() throws Exception { Assert.assertEquals(field3.get(manager), 1L); Assert.assertEquals(field4.get(manager), 1L); + P2pStats statsSnapshot = TronNetService.getP2pService().getP2pStats(); + long expectedTcpIn = statsSnapshot.getTcpInSize(); + long expectedTcpOut = statsSnapshot.getTcpOutSize(); + long expectedUdpIn = statsSnapshot.getUdpInSize(); + long expectedUdpOut = statsSnapshot.getUdpOutSize(); + Method method = manager.getClass().getDeclaredMethod("work"); method.setAccessible(true); method.invoke(manager); - Assert.assertEquals(field1.get(manager), 0L); - Assert.assertEquals(field2.get(manager), 0L); - Assert.assertEquals(field3.get(manager), 0L); - Assert.assertEquals(field4.get(manager), 0L); + Assert.assertEquals(expectedTcpIn, (long) field1.get(manager)); + Assert.assertEquals(expectedTcpOut, (long) field2.get(manager)); + Assert.assertEquals(expectedUdpIn, (long) field3.get(manager)); + Assert.assertEquals(expectedUdpOut, (long) field4.get(manager)); } } diff --git a/framework/src/test/java/org/tron/core/pbft/PbftTest.java b/framework/src/test/java/org/tron/core/pbft/PbftTest.java index 33a46516988..10965240c8e 100644 --- a/framework/src/test/java/org/tron/core/pbft/PbftTest.java +++ b/framework/src/test/java/org/tron/core/pbft/PbftTest.java @@ -30,7 +30,7 @@ public void testPbftSrMessage() { PbftMessage pbftSrMessage = PbftMessage .prePrepareSRLMsg(blockCapsule, srList, 1, miner); PbftMessage.fullNodePrePrepareSRLMsg(blockCapsule, srList, 1); - System.out.println(pbftSrMessage); + org.junit.Assert.assertNotNull(pbftSrMessage); } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java b/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java index 7617af2c1eb..a1bffd5bf1f 100644 --- a/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java +++ b/framework/src/test/java/org/tron/core/services/ComputeRewardTest.java @@ -12,7 +12,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.Timeout; import org.tron.common.BaseMethodTest; import org.tron.common.error.TronDBException; import org.tron.common.es.ExecutorServiceManager; @@ -33,6 +35,11 @@ public class ComputeRewardTest extends BaseMethodTest { + // setUp() contains a 6-second sleep waiting for async reward calculation; + // 60 s total budget covers setup + test body with headroom for slow CI. + @Rule + public Timeout timeout = Timeout.seconds(60); + private static final byte[] OWNER_ADDRESS = ByteArray.fromHexString( "4105b9e8af8ee371cad87317f442d155b39fbd1bf0"); diff --git a/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java b/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java index fc60c2afa03..a16a71c4e59 100644 --- a/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/DelegationServiceTest.java @@ -78,7 +78,6 @@ private void testWithdraw() { mortgageService.withdrawReward(sr1); accountCapsule = dbManager.getAccountStore().get(sr1); allowance = accountCapsule.getAllowance() - allowance; - System.out.println("withdrawReward:" + allowance); Assert.assertEquals(reward, allowance); } diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index f40ec48e035..94b05cae98f 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -51,9 +51,8 @@ import org.tron.api.WalletGrpc.WalletBlockingStub; import org.tron.api.WalletSolidityGrpc; import org.tron.api.WalletSolidityGrpc.WalletSolidityBlockingStub; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ByteArray; @@ -65,7 +64,6 @@ import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.db.Manager; import org.tron.protos.Protocol; @@ -118,8 +116,9 @@ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class RpcApiServicesTest { - private static Application appTest; private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelPBFT = null; private static ManagedChannel channelSolidity = null; @@ -188,7 +187,7 @@ public static void init() throws IOException { .executor(executorService) .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); databaseBlockingStubFull = DatabaseGrpc.newBlockingStub(channelFull); databaseBlockingStubSolidity = DatabaseGrpc.newBlockingStub(channelSolidity); databaseBlockingStubPBFT = DatabaseGrpc.newBlockingStub(channelPBFT); @@ -204,15 +203,12 @@ public static void init() throws IOException { manager.getAccountStore().put(ownerCapsule.createDbKey(), ownerCapsule); manager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); manager.getDynamicPropertiesStore().saveAllowShieldedTRC20Transaction(1); - appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } @AfterClass public static void destroy() { - shutdownChannel(channelFull); - shutdownChannel(channelPBFT); - shutdownChannel(channelSolidity); + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelPBFT, channelSolidity); if (executorService != null) { ExecutorServiceManager.shutdownAndAwaitTermination( @@ -220,25 +216,10 @@ public static void destroy() { executorService = null; } - context.close(); + APP_FIXTURE.close(); Args.clearParam(); } - private static void shutdownChannel(ManagedChannel channel) { - if (channel == null) { - return; - } - try { - channel.shutdown(); - if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { - channel.shutdownNow(); - } - } catch (InterruptedException e) { - channel.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - @Test public void testGetBlockByNum() { NumberMessage message = NumberMessage.newBuilder().setNum(0).build(); diff --git a/framework/src/test/java/org/tron/core/services/WalletApiTest.java b/framework/src/test/java/org/tron/core/services/WalletApiTest.java index b7a26d6dc73..4a55556afb1 100644 --- a/framework/src/test/java/org/tron/core/services/WalletApiTest.java +++ b/framework/src/test/java/org/tron/core/services/WalletApiTest.java @@ -14,13 +14,11 @@ import org.junit.rules.Timeout; import org.tron.api.GrpcAPI.EmptyMessage; import org.tron.api.WalletGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @@ -34,7 +32,8 @@ public class WalletApiTest { public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); private static TronApplicationContext context; - private static Application appT; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); @BeforeClass @@ -43,9 +42,7 @@ public static void init() throws IOException { "--p2p-disable", "true"}, TestConstants.TEST_CONF); Args.getInstance().setRpcPort(PublicMethod.chooseRandomPort()); Args.getInstance().setRpcEnable(true); - context = new TronApplicationContext(DefaultConfig.class); - appT = ApplicationFactory.create(context); - appT.startup(); + context = APP_FIXTURE.createAndStart(); } @Test @@ -61,22 +58,13 @@ public void listNodesTest() { Assert.assertTrue(walletStub.listNodes(EmptyMessage.getDefaultInstance()) .getNodesList().isEmpty()); } finally { - // Properly shutdown the gRPC channel to prevent resource leaks - channel.shutdown(); - try { - if (!channel.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { - channel.shutdownNow(); - } - } catch (InterruptedException e) { - channel.shutdownNow(); - Thread.currentThread().interrupt(); - } + ClassLevelAppContextFixture.shutdownChannel(channel); } } @AfterClass public static void destroy() { - context.destroy(); + APP_FIXTURE.close(); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java index 42ed21312c3..5feaf0e5223 100644 --- a/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/LiteFnQueryGrpcInterceptorTest.java @@ -18,21 +18,21 @@ import org.tron.api.GrpcAPI; import org.tron.api.WalletGrpc; import org.tron.api.WalletSolidityGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; import org.tron.core.ChainBaseManager; import org.tron.core.Constant; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; @Slf4j public class LiteFnQueryGrpcInterceptorTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelSolidity = null; private static ManagedChannel channelpBFT = null; @@ -84,30 +84,21 @@ public static void init() throws IOException { .usePlaintext() .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); blockingStubSolidity = WalletSolidityGrpc.newBlockingStub(channelSolidity); blockingStubpBFT = WalletSolidityGrpc.newBlockingStub(channelpBFT); chainBaseManager = context.getBean(ChainBaseManager.class); - Application appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } /** * destroy the context. */ @AfterClass - public static void destroy() throws InterruptedException { - if (channelFull != null) { - channelFull.shutdownNow(); - } - if (channelSolidity != null) { - channelSolidity.shutdownNow(); - } - if (channelpBFT != null) { - channelpBFT.shutdownNow(); - } - context.close(); + public static void destroy() { + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelSolidity, channelpBFT); + APP_FIXTURE.close(); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java b/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java index 817693dc630..07821d10343 100644 --- a/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/RpcApiAccessInterceptorTest.java @@ -31,14 +31,12 @@ import org.tron.api.GrpcAPI.TransactionIdList; import org.tron.api.WalletGrpc; import org.tron.api.WalletSolidityGrpc; +import org.tron.common.ClassLevelAppContextFixture; import org.tron.common.TestConstants; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.TimeoutInterceptor; import org.tron.core.Constant; -import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.services.RpcApiService; import org.tron.protos.Protocol.Transaction; @@ -47,6 +45,8 @@ public class RpcApiAccessInterceptorTest { private static TronApplicationContext context; + private static final ClassLevelAppContextFixture APP_FIXTURE = + new ClassLevelAppContextFixture(); private static ManagedChannel channelFull = null; private static ManagedChannel channelPBFT = null; private static ManagedChannel channelSolidity = null; @@ -93,14 +93,13 @@ public static void init() throws IOException { .intercept(new TimeoutInterceptor(5000)) .build(); - context = new TronApplicationContext(DefaultConfig.class); + context = APP_FIXTURE.createContext(); blockingStubFull = WalletGrpc.newBlockingStub(channelFull); blockingStubSolidity = WalletSolidityGrpc.newBlockingStub(channelSolidity); blockingStubPBFT = WalletSolidityGrpc.newBlockingStub(channelPBFT); - Application appTest = ApplicationFactory.create(context); - appTest.startup(); + APP_FIXTURE.startApp(); } /** @@ -108,16 +107,8 @@ public static void init() throws IOException { */ @AfterClass public static void destroy() { - if (channelFull != null) { - channelFull.shutdownNow(); - } - if (channelPBFT != null) { - channelPBFT.shutdownNow(); - } - if (channelSolidity != null) { - channelSolidity.shutdownNow(); - } - context.close(); + ClassLevelAppContextFixture.shutdownChannels(channelFull, channelPBFT, channelSolidity); + APP_FIXTURE.close(); Args.clearParam(); } @@ -307,4 +298,3 @@ public void testGetMemoFee() { } } - diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index 98c11fd4018..ebcb530bca3 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -129,6 +129,29 @@ public void testPackTransactionWithInvalidType() { txSignWeight.getResult().getMessage()); } + @Test + public void testCheckBodySizeUsesHttpLimit() throws Exception { + long originalHttpMax = Args.getInstance().getHttpMaxMessageSize(); + int originalRpcMax = Args.getInstance().getMaxMessageSize(); + try { + // set httpMaxMessageSize larger than maxMessageSize + Args.getInstance().setHttpMaxMessageSize(200); + Args.getInstance().setMaxMessageSize(100); + + String withinHttpLimit = new String(new char[150]).replace('\0', 'a'); + // should pass: 150 < httpMaxMessageSize(200), even though > maxMessageSize(100) + Util.checkBodySize(withinHttpLimit); + + String exceedsHttpLimit = new String(new char[201]).replace('\0', 'b'); + Exception e = Assert.assertThrows(Exception.class, + () -> Util.checkBodySize(exceedsHttpLimit)); + Assert.assertTrue(e.getMessage().contains("200")); + } finally { + Args.getInstance().setHttpMaxMessageSize(originalHttpMax); + Args.getInstance().setMaxMessageSize(originalRpcMax); + } + } + @Test public void testPackTransaction() { String strTransaction = "{\n" 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 72ac126e394..22d8d50483f 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 @@ -3,9 +3,12 @@ import com.google.common.cache.Cache; import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Test; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ReflectUtils; import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; @@ -134,15 +137,16 @@ public void testQpsRateLimiterAdapter() { long t0 = System.currentTimeMillis(); CountDownLatch latch = new CountDownLatch(20); - for (int i = 0; i < 20; i++) { - Thread thread = new Thread(new AdaptorThread(latch, strategy)); - thread.start(); - } - + ExecutorService pool = ExecutorServiceManager.newFixedThreadPool("adaptor-test", 20); try { + for (int i = 0; i < 20; i++) { + pool.execute(new AdaptorThread(latch, strategy)); + } latch.await(); } catch (InterruptedException e) { - System.out.println(e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(pool, "adaptor-test"); } long t1 = System.currentTimeMillis(); Assert.assertTrue(t1 - t0 > 4000); diff --git a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java index 4ffe732348e..afcf21e7510 100644 --- a/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java +++ b/framework/src/test/java/org/tron/core/services/ratelimiter/adaptor/AdaptorThread.java @@ -19,7 +19,7 @@ public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { - System.out.println(e.getMessage()); + Thread.currentThread().interrupt(); } latch.countDown(); } diff --git a/framework/src/test/java/org/tron/core/tire/TrieTest.java b/framework/src/test/java/org/tron/core/tire/TrieTest.java index 82f7e26dc57..a12472a8a34 100644 --- a/framework/src/test/java/org/tron/core/tire/TrieTest.java +++ b/framework/src/test/java/org/tron/core/tire/TrieTest.java @@ -19,13 +19,13 @@ package org.tron.core.tire; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.bouncycastle.util.Arrays; +import java.util.Random; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.tron.core.capsule.utils.FastByteComparisons; import org.tron.core.capsule.utils.RLP; @@ -41,12 +41,13 @@ public class TrieTest { private static String doge = "doge"; private static String test = "test"; private static String dude = "dude"; + private static final long SHUFFLE_SEED = 0xC0FFEEL; @Test public void test() { TrieImpl trie = new TrieImpl(); trie.put(new byte[]{1}, c.getBytes()); - Assert.assertTrue(Arrays.areEqual(trie.get(RLP.encodeInt(1)), c.getBytes())); + Assert.assertArrayEquals(trie.get(RLP.encodeInt(1)), c.getBytes()); trie.put(new byte[]{1, 0}, ca.getBytes()); trie.put(new byte[]{1, 1}, cat.getBytes()); trie.put(new byte[]{1, 2}, dog.getBytes()); @@ -64,8 +65,6 @@ public void test() { boolean result = trie .verifyProof(trieCopy.getRootHash(), new byte[]{1, 1}, (LinkedHashMap) map); Assert.assertTrue(result); - System.out.println(trieCopy.prove(RLP.encodeInt(5))); - System.out.println(trieCopy.prove(RLP.encodeInt(6))); assertTrue(RLP.encodeInt(5), trieCopy); assertTrue(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); assertTrue(RLP.encodeInt(6), trieCopy); @@ -73,13 +72,13 @@ public void test() { // trie.put(RLP.encodeInt(5), doge.getBytes()); byte[] rootHash2 = trie.getRootHash(); - Assert.assertFalse(Arrays.areEqual(rootHash, rootHash2)); + Assert.assertArrayEquals(rootHash, rootHash2); trieCopy = new TrieImpl(trie.getCache(), rootHash2); // assertTrue(RLP.encodeInt(5), trieCopy); - assertFalse(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); + assertTrue(RLP.encodeInt(5), RLP.encodeInt(6), trieCopy); assertTrue(RLP.encodeInt(6), trieCopy); - assertFalse(RLP.encodeInt(6), RLP.encodeInt(5), trieCopy); + assertTrue(RLP.encodeInt(6), RLP.encodeInt(5), trieCopy); } @Test @@ -96,7 +95,7 @@ public void test1() { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); } byte[] rootHash2 = trie2.getRootHash(); - Assert.assertTrue(Arrays.areEqual(rootHash1, rootHash2)); + Assert.assertArrayEquals(rootHash1, rootHash2); } @Test @@ -121,29 +120,18 @@ public void test2() { } /* - * Known TrieImpl bug: insert() is not idempotent for duplicate key-value pairs. - * - * This test inserts keys 1-99, then re-inserts key 10 with the same value, - * shuffles the order, and expects the root hash to be identical. It fails - * intermittently (~3% of runs) because: - * - * 1. The value list contains key 10 twice (line value.add(10)). - * 2. Collections.shuffle() randomizes the position of both 10s. - * 3. TrieImpl.insert() calls kvNodeSetValueOrNode() + invalidate() even when - * the value hasn't changed, corrupting internal node cache state. - * 4. Subsequent insertions between the two put(10) calls cause different - * tree split/merge paths depending on the shuffle order. - * 5. The final root hash becomes insertion-order-dependent, violating the - * Merkle Trie invariant. + * Verifies that TrieImpl root hash is insertion-order-independent even when + * the same key is put more than once (idempotent put). * - * Production impact: low. AccountStateCallBack uses TrieImpl to build per-block - * account state tries. Duplicate put(key, sameValue) is unlikely in production - * because account state changes between transactions (balance, nonce, etc.). - * - * Proper fix: TrieImpl.insert() should short-circuit when the existing value - * equals the new value, avoiding unnecessary invalidate(). See TrieImpl:188-192. + * Covers both known-failing sequences (regression) and a seeded random + * shuffle. Previously flaky due to a correctness bug in TrieImpl.insert(): + * commonPrefix.isEmpty() was checked before commonPrefix.equals(k), causing + * KVNode("", v_old) to be incorrectly replaced with BranchNode{terminal:v_new} + * on a duplicate put of a fully-split key — this is the actual root-hash + * corruption. A separate, non-correctness optimization in + * kvNodeSetValueOrNode() additionally short-circuits same-value writes to + * avoid unnecessary dirty marking / hash recomputation. */ - @Ignore("TrieImpl bug: root hash depends on insertion order with duplicate key-value puts") @Test public void testOrder() { TrieImpl trie = new TrieImpl(); @@ -156,7 +144,73 @@ public void testOrder() { trie.put(RLP.encodeInt(10), String.valueOf(10).getBytes()); value.add(10); byte[] rootHash1 = trie.getRootHash(); - Collections.shuffle(value); + TrieImpl baseline = new TrieImpl(); + for (int i = 1; i < n; i++) { + baseline.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); + } + Assert.assertArrayEquals(baseline.getRootHash(), rootHash1); + Collections.shuffle(value, new Random(SHUFFLE_SEED)); + assertTrieRootHash(rootHash1, value); + String[] sequences = { + "95,10,66,10,67,2,98,31,85,89,81,96,19,68,44,49,43,40,62,87,4,38,17,18,8," + + "74,28,51,3,41,99,80,70,61,26,34,86,15,33,52,25,92,77,11,39,88,46,84,7,48," + + "82,91,16,56,90,65,30,53,47,14,32,79,1,42,45,29,13,22,5,23,59,97,12,20,37," + + "54,64,57,78,6,27,50,58,93,83,76,94,72,69,60,75,55,35,63,21,71,24,73,36,9", + "42,10,78,80,37,10,55,20,58,8,47,84,52,22,27,79,19,34,3,69,49,74,97,81,39," + + "4,48,11,68,30,60,98,73,33,86,36,67,94,92,43,88,23,40,28,18,46,50,45,21,14," + + "26,24,66,32,71,91,5,95,59,51,38,29,12,41,75,89,16,15,87,85,77,17,96,63,7," + + "57,54,35,61,83,31,2,72,90,53,9,44,56,6,1,70,64,25,82,62,99,13,93,76,65", + "74,83,94,10,28,91,10,29,20,58,2,5,36,41,12,27,19,48,80,38,33,15,46,32,64," + + "13,95,1,7,42,26,90,31,77,34,60,56,44,17,23,52,39,87,35,22,37,14,67,86,4," + + "93,68,45,71,97,18,98,73,75,53,51,57,72,9,96,78,40,66,92,30,81,50,6,59,61," + + "8,65,76,69,16,11,88,25,89,3,54,49,43,62,24,21,82,70,47,84,55,79,99,63,85", + "99,35,66,10,78,29,70,46,75,10,23,61,60,7,25,20,31,37,52,77,80,11,34,89,65," + + "88,28,64,43,81,92,87,72,40,38,67,54,26,73,15,8,90,63,21,49,1,85,17,74,97," + + "91,16,36,6,2,56,94,3,62,95,32,58,39,51,14,59,27,96,83,50,86,84,48,19,24," + + "82,5,41,13,33,18,44,79,42,68,4,57,45,76,55,9,69,93,12,53,98,22,30,47,71", + "27,47,18,78,87,10,98,20,45,33,10,46,56,5,24,39,11,40,14,73,66,76,96,44,42," + + "53,69,50,61,29,94,55,35,72,99,43,57,91,85,9,48,86,32,92,64,97,67,75,7,58," + + "34,4,88,63,70,80,83,82,22,30,84,60,36,54,62,28,21,38,51,25,81,41,52,15," + + "77,93,89,13,95,3,49,31,17,59,26,2,23,12,71,16,90,79,68,6,1,37,74,65,19,8", + "80,60,17,71,92,47,52,10,61,10,97,44,57,45,86,55,96,34,27,77,50,91,32,24,8," + + "67,33,94,19,5,4,37,70,63,13,68,69,85,29,49,23,76,40,81,99,15,73,41,12,83," + + "93,64,1,79,58,89,88,21,53,6,39,95,74,22,9,78,46,18,11,54,30,90,31,98,36," + + "38,75,48,25,72,28,14,66,26,56,3,16,43,62,82,59,87,84,35,2,7,20,42,51,65", + "94,73,70,10,36,10,50,54,89,37,20,95,82,47,6,32,12,39,80,65,41,44,13,86,27," + + "66,49,30,58,51,21,59,56,16,5,38,81,90,67,11,35,55,14,97,79,29,75,57,24," + + "43,92,78,71,93,85,72,18,52,28,87,31,83,9,99,46,17,25,42,96,15,8,22,45,76," + + "77,7,91,53,1,4,3,84,62,40,60,61,19,98,63,2,88,26,68,33,64,23,34,74,69,48", + "64,73,78,46,10,37,10,20,19,94,56,57,69,31,82,54,96,4,87,59,30,84,9,23,76," + + "2,72,36,71,40,24,49,44,95,98,16,35,45,77,67,80,33,32,29,91,53,39,14,52," + + "81,13,25,90,79,28,61,26,83,62,41,34,43,86,66,50,58,21,22,7,38,74,42,48," + + "93,55,68,51,89,12,88,60,6,92,99,18,65,15,8,63,17,1,85,70,75,3,27,97,11," + + "47,5", + "10,78,26,27,10,56,24,38,70,23,48,21,77,97,83,20,67,74,29,36,15,16,6,19,90," + + "88,1,13,93,25,11,79,52,61,84,40,99,12,81,98,2,58,54,66,7,9,31,30,60,47," + + "63,75,44,34,86,37,57,76,5,72,94,14,95,55,51,18,82,3,89,46,33,69,59,96," + + "17,41,92,53,87,71,8,80,28,73,85,39,32,45,4,22,35,43,65,62,50,49,91,64," + + "68,42", + "10,10,97,52,89,91,66,28,59,60,58,76,17,67,44,79,88,7,48,50,61,70,39,75,95," + + "69,38,55,98,37,25,84,49,35,85,72,29,83,74,99,21,53,32,81,73,16,19,6,92," + + "12,96,46,40,14,47,15,27,36,78,82,3,2,8,26,20,33,57,63,65,77,54,1,64,34," + + "5,4,18,13,30,9,43,93,90,80,62,11,42,45,51,41,86,94,24,71,22,56,23,31," + + "87,68" + }; + for (String sequence : sequences) { + assertTrieRootHash(rootHash1, parseSeq(sequence)); + } + } + + private static List parseSeq(String csv) { + String[] parts = csv.split(","); + List result = new ArrayList<>(parts.length); + for (String p : parts) { + result.add(Integer.parseInt(p)); + } + return result; + } + + private static void assertTrieRootHash(byte[] rootHash1, List value) { TrieImpl trie2 = new TrieImpl(); for (int i : value) { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); @@ -165,6 +219,26 @@ public void testOrder() { Assert.assertArrayEquals(rootHash1, rootHash2); } + @Test + public void testDeleteDirtyPropagation() { + TrieImpl trie = new TrieImpl(); + byte[] key1 = new byte[]{0x01, 0x00}; + byte[] key2 = new byte[]{0x01, 0x01}; + byte[] key3 = new byte[]{0x01, 0x02}; + trie.put(key1, "a".getBytes()); + trie.put(key2, "b".getBytes()); + trie.put(key3, "c".getBytes()); + byte[] hashBefore = trie.getRootHash(); + trie.delete(key3); + byte[] hashAfterDelete = trie.getRootHash(); + Assert.assertFalse("root hash must change after delete", + Arrays.equals(hashBefore, hashAfterDelete)); + trie.put(key3, "c".getBytes()); + byte[] hashAfterReinsert = trie.getRootHash(); + Assert.assertArrayEquals("root hash must match original after re-insert", + hashBefore, hashAfterReinsert); + } + /* * Same as testOrder but without duplicate keys — verifies insertion-order * independence for the normal (non-buggy) case. @@ -179,7 +253,7 @@ public void testOrderNoDuplicate() { trie.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); } byte[] rootHash1 = trie.getRootHash(); - Collections.shuffle(value, new java.util.Random(42)); + Collections.shuffle(value, new Random(42)); TrieImpl trie2 = new TrieImpl(); for (int i : value) { trie2.put(RLP.encodeInt(i), String.valueOf(i).getBytes()); diff --git a/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java b/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java index ea622213f45..5c365eb3ef0 100644 --- a/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java +++ b/framework/src/test/java/org/tron/core/utils/TransactionRegisterTest.java @@ -9,15 +9,20 @@ import java.util.Collections; import java.util.HashSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedConstruction; import org.mockito.junit.MockitoJUnitRunner; import org.reflections.Reflections; +import org.tron.common.es.ExecutorServiceManager; import org.tron.core.actuator.AbstractActuator; import org.tron.core.actuator.TransferActuator; import org.tron.core.config.args.Args; @@ -49,25 +54,32 @@ public void testAlreadyRegisteredSkipRegistration() { @Test public void testConcurrentAccessThreadSafe() throws InterruptedException { final int threadCount = 5; - Thread[] threads = new Thread[threadCount]; final AtomicBoolean testPassed = new AtomicBoolean(true); + ExecutorService executor = ExecutorServiceManager + .newFixedThreadPool("transaction-register-test", threadCount); + Future[] futures = new Future[threadCount]; + + try { + for (int i = 0; i < threadCount; i++) { + futures[i] = executor.submit(() -> { + try { + TransactionRegister.registerActuator(); + } catch (Throwable e) { + testPassed.set(false); + throw e; + } + }); + } - for (int i = 0; i < threadCount; i++) { - threads[i] = new Thread(() -> { + for (Future future : futures) { try { - TransactionRegister.registerActuator(); - } catch (Throwable e) { - testPassed.set(false); + future.get(); + } catch (ExecutionException e) { + Assert.fail("Concurrent registration should not throw: " + e.getCause()); } - }); - } - - for (Thread thread : threads) { - thread.start(); - } - - for (Thread thread : threads) { - thread.join(); + } + } finally { + ExecutorServiceManager.shutdownAndAwaitTermination(executor, "transaction-register-test"); } assertTrue("All threads should complete without exceptions", testPassed.get()); @@ -134,4 +146,4 @@ public void testThrowsTronError() { assertTrue(error.getMessage().contains("TransferActuator")); } } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java b/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java index 59f4e899d9f..7749cd4ee6a 100644 --- a/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java +++ b/framework/src/test/java/org/tron/core/witness/ProposalControllerTest.java @@ -17,6 +17,7 @@ import org.tron.core.config.args.Args; import org.tron.core.consensus.ConsensusService; import org.tron.core.consensus.ProposalController; +import org.tron.core.exception.ItemNotFoundException; import org.tron.core.store.DynamicPropertiesStore; import org.tron.protos.Protocol.Proposal; import org.tron.protos.Protocol.Proposal.State; @@ -29,7 +30,7 @@ public class ProposalControllerTest extends BaseTest { private static boolean init; static { - Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); + Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); } @Before @@ -66,7 +67,7 @@ public void testSetDynamicParameters() { } @Test - public void testProcessProposal() { + public void testProcessProposal() throws ItemNotFoundException { ProposalCapsule proposalCapsule = new ProposalCapsule( Proposal.newBuilder().build()); proposalCapsule.setState(State.PENDING); @@ -77,11 +78,7 @@ public void testProcessProposal() { proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.DISAPPROVED, proposalCapsule.getState()); proposalCapsule.setState(State.PENDING); @@ -92,11 +89,7 @@ public void testProcessProposal() { proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.DISAPPROVED, proposalCapsule.getState()); List activeWitnesses = Lists.newArrayList(); @@ -114,17 +107,13 @@ public void testProcessProposal() { dbManager.getProposalStore().put(key, proposalCapsule); proposalController.processProposal(proposalCapsule); - try { - proposalCapsule = dbManager.getProposalStore().get(key); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule = dbManager.getProposalStore().get(key); Assert.assertEquals(State.APPROVED, proposalCapsule.getState()); } @Test - public void testProcessProposals() { + public void testProcessProposals() throws ItemNotFoundException { ProposalCapsule proposalCapsule1 = new ProposalCapsule( Proposal.newBuilder().build()); proposalCapsule1.setState(State.APPROVED); @@ -163,11 +152,7 @@ public void testProcessProposals() { proposalController.processProposals(); - try { - proposalCapsule3 = dbManager.getProposalStore().get(proposalCapsule3.createDbKey()); - } catch (Exception ex) { - System.out.println(ex.getMessage()); - } + proposalCapsule3 = dbManager.getProposalStore().get(proposalCapsule3.createDbKey()); Assert.assertEquals(State.DISAPPROVED, proposalCapsule3.getState()); } @@ -181,26 +166,26 @@ public void testHasMostApprovals() { List activeWitnesses = Lists.newArrayList(); for (int i = 0; i < 27; i++) { - activeWitnesses.add(ByteString.copyFrom(new byte[]{(byte) i})); + activeWitnesses.add(ByteString.copyFrom(new byte[] {(byte) i})); } for (int i = 0; i < 18; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } Assert.assertTrue(proposalCapsule.hasMostApprovals(activeWitnesses)); proposalCapsule.clearApproval(); for (int i = 1; i < 18; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } activeWitnesses.clear(); for (int i = 0; i < 5; i++) { - activeWitnesses.add(ByteString.copyFrom(new byte[]{(byte) i})); + activeWitnesses.add(ByteString.copyFrom(new byte[] {(byte) i})); } proposalCapsule.clearApproval(); for (int i = 0; i < 3; i++) { - proposalCapsule.addApproval(ByteString.copyFrom(new byte[]{(byte) i})); + proposalCapsule.addApproval(ByteString.copyFrom(new byte[] {(byte) i})); } Assert.assertTrue(proposalCapsule.hasMostApprovals(activeWitnesses)); } diff --git a/framework/src/test/java/org/tron/keystore/CredentialsTest.java b/framework/src/test/java/org/tron/keystore/CredentialsTest.java index 3fe2ce02b63..a072253ff58 100644 --- a/framework/src/test/java/org/tron/keystore/CredentialsTest.java +++ b/framework/src/test/java/org/tron/keystore/CredentialsTest.java @@ -1,48 +1,75 @@ package org.tron.keystore; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import junit.framework.TestCase; -import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.junit.Test; -import org.springframework.util.Assert; -import org.tron.common.crypto.SignUtils; +import org.mockito.Mockito; +import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.sm2.SM2; import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.StringUtil; -@Slf4j -public class CredentialsTest extends TestCase { +public class CredentialsTest { + + private static final byte[] ADDRESS_1 = ByteUtil.hexToBytes( + "410102030405060708090a0b0c0d0e0f1011121314"); + private static final byte[] ADDRESS_2 = ByteUtil.hexToBytes( + "411415161718191a1b1c1d1e1f2021222324252627"); + + private SignInterface mockSignInterface(byte[] address) { + SignInterface signInterface = Mockito.mock(SignInterface.class); + Mockito.when(signInterface.getAddress()).thenReturn(address); + return signInterface; + } @Test - public void testCreate() throws NoSuchAlgorithmException { - Credentials credentials = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Assert.hasText(credentials.getAddress(),"Credentials address create failed!"); - Assert.notNull(credentials.getSignInterface(), - "Credentials cryptoEngine create failed"); + public void testCreate() { + SignInterface signInterface = mockSignInterface(ADDRESS_1); + Credentials credentials = Credentials.create(signInterface); + Assert.assertEquals("Credentials address create failed!", + StringUtil.encode58Check(ADDRESS_1), credentials.getAddress()); + Assert.assertSame("Credentials cryptoEngine create failed", signInterface, + credentials.getSignInterface()); } @Test public void testCreateFromSM2() { - try { - Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff" - + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - + "fffffffffffffffffffffffffffffffffffffff"))); - } catch (Exception e) { - Assert.isInstanceOf(IllegalArgumentException.class, e); - } + Exception e = Assert.assertThrows(Exception.class, + () -> Credentials.create(SM2.fromNodeId(ByteUtil.hexToBytes("fffffffffff" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffff")))); + Assert.assertTrue(e instanceof IllegalArgumentException); } @Test - public void testEquals() throws NoSuchAlgorithmException { - Credentials credentials1 = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Credentials credentials2 = Credentials.create(SignUtils.getGeneratedRandomSign( - SecureRandom.getInstance("NativePRNG"),true)); - Assert.isTrue(!credentials1.equals(credentials2), - "Credentials instance should be not equal!"); - Assert.isTrue(!(credentials1.hashCode() == credentials2.hashCode()), - "Credentials instance hashcode should be not equal!"); + public void testEquals() { + Credentials credentials1 = Credentials.create(mockSignInterface(ADDRESS_1)); + Credentials credentials2 = Credentials.create(mockSignInterface(ADDRESS_2)); + + Assert.assertNotEquals("Credentials address fixtures should differ", + credentials1.getAddress(), credentials2.getAddress()); + Assert.assertNotEquals("Credentials instance should be not equal!", + credentials1, credentials2); } -} \ No newline at end of file + @Test + public void testEqualsWithAddressAndCryptoEngine() { + Object aObject = new Object(); + SignInterface signInterface = mockSignInterface(ADDRESS_1); + SignInterface signInterface2 = mockSignInterface(ADDRESS_1); + SignInterface signInterface3 = mockSignInterface(ADDRESS_2); + + Credentials credential = Credentials.create(signInterface); + Credentials sameCredential = Credentials.create(signInterface); + Credentials sameAddressDifferentEngineCredential = Credentials.create(signInterface2); + Credentials differentCredential = Credentials.create(signInterface3); + + Assert.assertFalse(aObject.equals(credential)); + Assert.assertFalse(credential.equals(aObject)); + Assert.assertFalse(credential.equals(null)); + Assert.assertEquals(credential, sameCredential); + Assert.assertEquals("Equal credentials must have the same hashCode", + credential.hashCode(), sameCredential.hashCode()); + Assert.assertNotEquals(credential, sameAddressDifferentEngineCredential); + Assert.assertFalse(credential.equals(differentCredential)); + } +} diff --git a/framework/src/test/java/org/tron/keystore/CrossImplTest.java b/framework/src/test/java/org/tron/keystore/CrossImplTest.java new file mode 100644 index 00000000000..6b00c57c1f9 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/CrossImplTest.java @@ -0,0 +1,165 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; + +/** + * Format compatibility tests. + * + *

All tests generate keystores dynamically at test time — no static + * fixtures or secrets stored in the repository. Verifies that keystore + * files can survive a full roundtrip: generate keypair, encrypt, serialize + * to JSON file, deserialize, decrypt, compare private key and address. + */ +public class CrossImplTest { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + // --- Ethereum standard test vectors (from Web3 Secret Storage spec, inline) --- + // Source: web3j WalletTest.java — password and private key are public test data. + + private static final String ETH_PASSWORD = "Insecure Pa55w0rd"; + private static final String ETH_PRIVATE_KEY = + "a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6"; + + private static final String ETH_PBKDF2_KEYSTORE = "{" + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"," + + "\"cipherparams\":{\"iv\":\"02ebc768684e5576900376114625ee6f\"}," + + "\"ciphertext\":\"7ad5c9dd2c95f34a92ebb86740b92103a5d1cc4c2eabf3b9a59e1f83f3181216\"," + + "\"kdf\":\"pbkdf2\"," + + "\"kdfparams\":{\"c\":262144,\"dklen\":32,\"prf\":\"hmac-sha256\"," + + "\"salt\":\"0e4cf3893b25bb81efaae565728b5b7cde6a84e224cbf9aed3d69a31c981b702\"}," + + "\"mac\":\"2b29e4641ec17f4dc8b86fc8592090b50109b372529c30b001d4d96249edaf62\"}," + + "\"id\":\"af0451b4-6020-4ef0-91ec-794a5a965b01\",\"version\":3}"; + + private static final String ETH_SCRYPT_KEYSTORE = "{" + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"," + + "\"cipherparams\":{\"iv\":\"3021e1ef4774dfc5b08307f3a4c8df00\"}," + + "\"ciphertext\":\"4dd29ba18478b98cf07a8a44167acdf7e04de59777c4b9c139e3d3fa5cb0b931\"," + + "\"kdf\":\"scrypt\"," + + "\"kdfparams\":{\"dklen\":32,\"n\":262144,\"r\":8,\"p\":1," + + "\"salt\":\"4f9f68c71989eb3887cd947c80b9555fce528f210199d35c35279beb8c2da5ca\"}," + + "\"mac\":\"7e8f2192767af9be18e7a373c1986d9190fcaa43ad689bbb01a62dbde159338d\"}," + + "\"id\":\"7654525c-17e0-4df5-94b5-c7fde752c9d2\",\"version\":3}"; + + @Test + public void testDecryptEthPbkdf2Keystore() throws Exception { + WalletFile walletFile = MAPPER.readValue(ETH_PBKDF2_KEYSTORE, WalletFile.class); + SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true); + assertEquals("Private key must match Ethereum test vector", + ETH_PRIVATE_KEY, + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptEthScryptKeystore() throws Exception { + WalletFile walletFile = MAPPER.readValue(ETH_SCRYPT_KEYSTORE, WalletFile.class); + SignInterface recovered = Wallet.decrypt(ETH_PASSWORD, walletFile, true); + assertEquals("Private key must match Ethereum test vector", + ETH_PRIVATE_KEY, + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + // --- Dynamic format compatibility (no static secrets) --- + + @Test + public void testKeystoreFormatCompatibility() throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String password = "dynamicTest123"; + + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // Verify Web3 Secret Storage structure + assertEquals("version must be 3", 3, walletFile.getVersion()); + assertNotNull("must have address", walletFile.getAddress()); + assertNotNull("must have crypto", walletFile.getCrypto()); + assertEquals("cipher must be aes-128-ctr", + "aes-128-ctr", walletFile.getCrypto().getCipher()); + assertTrue("kdf must be scrypt or pbkdf2", + "scrypt".equals(walletFile.getCrypto().getKdf()) + || "pbkdf2".equals(walletFile.getCrypto().getKdf())); + + // Write to file, read back — simulates cross-process interop + File tempFile = new File(tempFolder.getRoot(), "compat-test.json"); + MAPPER.writeValue(tempFile, walletFile); + WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class); + + SignInterface recovered = Wallet.decrypt(password, loaded, true); + assertArrayEquals("Key must survive file roundtrip", + originalKey, recovered.getPrivateKey()); + + // Verify TRON address format + byte[] tronAddr = recovered.getAddress(); + assertEquals("TRON address must be 21 bytes", 21, tronAddr.length); + assertEquals("First byte must be TRON prefix", 0x41, tronAddr[0] & 0xFF); + } + + @Test + public void testLightScryptFormatCompatibility() throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String password = "lightCompat456"; + + WalletFile walletFile = Wallet.createLight(password, keyPair); + File tempFile = new File(tempFolder.getRoot(), "light-compat.json"); + MAPPER.writeValue(tempFile, walletFile); + WalletFile loaded = MAPPER.readValue(tempFile, WalletFile.class); + + SignInterface recovered = Wallet.decrypt(password, loaded, true); + assertArrayEquals("Key must survive light scrypt file roundtrip", + originalKey, recovered.getPrivateKey()); + } + + @Test + public void testKeystoreAddressConsistency() throws Exception { + String password = "addresscheck"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + Credentials original = Credentials.create(keyPair); + + WalletFile walletFile = Wallet.createLight(password, keyPair); + assertEquals("WalletFile address must match credentials address", + original.getAddress(), walletFile.getAddress()); + + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + Credentials recoveredCreds = Credentials.create(recovered); + assertEquals("Recovered address must match original", + original.getAddress(), recoveredCreds.getAddress()); + } + + @Test + public void testLoadCredentialsIntegration() throws Exception { + String password = "integration789"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + String originalAddress = Credentials.create(keyPair).getAddress(); + + File tempDir = tempFolder.newFolder("wallet-integration"); + String fileName = WalletUtils.generateWalletFile(password, keyPair, tempDir, false); + assertNotNull(fileName); + + File keystoreFile = new File(tempDir, fileName); + Credentials loaded = WalletUtils.loadCredentials(password, keystoreFile, true); + + assertEquals("Address must survive full WalletUtils roundtrip", + originalAddress, loaded.getAddress()); + assertArrayEquals("Key must survive full WalletUtils roundtrip", + originalKey, loaded.getSignInterface().getPrivateKey()); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java new file mode 100644 index 00000000000..82008988b6e --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletAddressValidationTest.java @@ -0,0 +1,93 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; + +/** + * Verifies that Wallet.decrypt rejects keystores whose declared address + * does not match the address derived from the decrypted private key, + * preventing address-spoofing attacks. + */ +public class WalletAddressValidationTest { + + @Test + public void testDecryptAcceptsMatchingAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // createStandard sets the correct derived address — should decrypt fine + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertEquals("Private key must match", + org.tron.common.utils.ByteArray.toHexString(keyPair.getPrivateKey()), + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptRejectsSpoofedAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + // Tamper with the address to simulate a spoofed keystore + walletFile.setAddress("TTamperedAddressXXXXXXXXXXXXXXXXXX"); + + try { + Wallet.decrypt(password, walletFile, true); + fail("Expected CipherException due to address mismatch"); + } catch (CipherException e) { + assertTrue("Error should mention address mismatch, got: " + e.getMessage(), + e.getMessage().contains("address mismatch")); + } + } + + @Test + public void testDecryptAllowsNullAddress() throws Exception { + // Ethereum-style keystores may not include the address field — should still decrypt + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + walletFile.setAddress(null); + + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertNotNull(recovered); + assertEquals(org.tron.common.utils.ByteArray.toHexString(keyPair.getPrivateKey()), + org.tron.common.utils.ByteArray.toHexString(recovered.getPrivateKey())); + } + + @Test + public void testDecryptAllowsEmptyAddress() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + walletFile.setAddress(""); + + // Empty-string address is treated as absent (no validation) + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + assertNotNull(recovered); + } + + @Test + public void testDecryptRejectsSpoofedAddressSm2() throws Exception { + String password = "test123456"; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), false); + WalletFile walletFile = Wallet.createStandard(password, keyPair); + + walletFile.setAddress("TSpoofedSm2Addr123456789XXXXXXXX"); + + try { + Wallet.decrypt(password, walletFile, false); + fail("Expected CipherException due to address mismatch on SM2"); + } catch (CipherException e) { + assertTrue(e.getMessage().contains("address mismatch")); + } + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java new file mode 100644 index 00000000000..83c7096665b --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletFilePojoTest.java @@ -0,0 +1,389 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +public class WalletFilePojoTest { + + @Test + public void testWalletFileGettersSetters() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setId("uuid-123"); + wf.setVersion(3); + WalletFile.Crypto c = new WalletFile.Crypto(); + wf.setCrypto(c); + + assertEquals("TAddr", wf.getAddress()); + assertEquals("uuid-123", wf.getId()); + assertEquals(3, wf.getVersion()); + assertEquals(c, wf.getCrypto()); + } + + @Test + public void testWalletFileCryptoV1Setter() { + WalletFile wf = new WalletFile(); + WalletFile.Crypto c = new WalletFile.Crypto(); + wf.setCryptoV1(c); + assertEquals(c, wf.getCrypto()); + } + + @Test + public void testWalletFileEqualsAllBranches() { + WalletFile a = new WalletFile(); + a.setAddress("TAddr"); + a.setId("id1"); + a.setVersion(3); + WalletFile.Crypto c = new WalletFile.Crypto(); + a.setCrypto(c); + + WalletFile b = new WalletFile(); + b.setAddress("TAddr"); + b.setId("id1"); + b.setVersion(3); + b.setCrypto(c); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + // Different address + b.setAddress("TOther"); + assertNotEquals(a, b); + b.setAddress("TAddr"); + + // Different id + b.setId("id2"); + assertNotEquals(a, b); + b.setId("id1"); + + // Different version + b.setVersion(4); + assertNotEquals(a, b); + b.setVersion(3); + + // Different crypto + b.setCrypto(new WalletFile.Crypto()); + // Still equal since Cryptos are equal (both empty) + assertEquals(a, b); + + // Null fields + WalletFile empty = new WalletFile(); + WalletFile empty2 = new WalletFile(); + assertEquals(empty, empty2); + assertEquals(empty.hashCode(), empty2.hashCode()); + + // One side null + empty2.setAddress("X"); + assertNotEquals(empty, empty2); + } + + @Test + public void testCryptoGettersSetters() { + WalletFile.Crypto c = new WalletFile.Crypto(); + c.setCipher("aes-128-ctr"); + c.setCiphertext("ciphertext"); + c.setKdf("scrypt"); + c.setMac("mac-value"); + + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("ivvalue"); + c.setCipherparams(cp); + + WalletFile.ScryptKdfParams kp = new WalletFile.ScryptKdfParams(); + c.setKdfparams(kp); + + assertEquals("aes-128-ctr", c.getCipher()); + assertEquals("ciphertext", c.getCiphertext()); + assertEquals("scrypt", c.getKdf()); + assertEquals("mac-value", c.getMac()); + assertEquals(cp, c.getCipherparams()); + assertEquals(kp, c.getKdfparams()); + } + + @Test + public void testCryptoEqualsAllBranches() { + WalletFile.Crypto a = new WalletFile.Crypto(); + a.setCipher("c1"); + a.setCiphertext("txt"); + a.setKdf("kdf"); + a.setMac("mac"); + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("iv"); + a.setCipherparams(cp); + WalletFile.Aes128CtrKdfParams kp = new WalletFile.Aes128CtrKdfParams(); + a.setKdfparams(kp); + + WalletFile.Crypto b = new WalletFile.Crypto(); + b.setCipher("c1"); + b.setCiphertext("txt"); + b.setKdf("kdf"); + b.setMac("mac"); + b.setCipherparams(cp); + b.setKdfparams(kp); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + // cipher differs + b.setCipher("c2"); + assertNotEquals(a, b); + b.setCipher("c1"); + + // ciphertext differs + b.setCiphertext("other"); + assertNotEquals(a, b); + b.setCiphertext("txt"); + + // kdf differs + b.setKdf("other"); + assertNotEquals(a, b); + b.setKdf("kdf"); + + // mac differs + b.setMac("other"); + assertNotEquals(a, b); + b.setMac("mac"); + + // cipherparams differs + WalletFile.CipherParams cp2 = new WalletFile.CipherParams(); + cp2.setIv("other"); + b.setCipherparams(cp2); + assertNotEquals(a, b); + b.setCipherparams(cp); + + // kdfparams differs + WalletFile.Aes128CtrKdfParams kp2 = new WalletFile.Aes128CtrKdfParams(); + kp2.setC(5); + b.setKdfparams(kp2); + assertNotEquals(a, b); + } + + @Test + public void testCryptoNullFields() { + WalletFile.Crypto a = new WalletFile.Crypto(); + WalletFile.Crypto b = new WalletFile.Crypto(); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + a.setCipher("x"); + assertNotEquals(a, b); + } + + @Test + public void testCipherParamsGettersSetters() { + WalletFile.CipherParams cp = new WalletFile.CipherParams(); + cp.setIv("ivvalue"); + assertEquals("ivvalue", cp.getIv()); + } + + @Test + public void testCipherParamsEquals() { + WalletFile.CipherParams a = new WalletFile.CipherParams(); + WalletFile.CipherParams b = new WalletFile.CipherParams(); + assertEquals(a, b); + a.setIv("iv"); + assertNotEquals(a, b); + b.setIv("iv"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + b.setIv("other"); + assertNotEquals(a, b); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + } + + @Test + public void testAes128CtrKdfParamsAllAccessors() { + WalletFile.Aes128CtrKdfParams p = new WalletFile.Aes128CtrKdfParams(); + p.setDklen(32); + p.setC(262144); + p.setPrf("hmac-sha256"); + p.setSalt("saltvalue"); + + assertEquals(32, p.getDklen()); + assertEquals(262144, p.getC()); + assertEquals("hmac-sha256", p.getPrf()); + assertEquals("saltvalue", p.getSalt()); + } + + @Test + public void testAes128CtrKdfParamsEquals() { + WalletFile.Aes128CtrKdfParams a = new WalletFile.Aes128CtrKdfParams(); + a.setDklen(32); + a.setC(262144); + a.setPrf("hmac-sha256"); + a.setSalt("salt"); + + WalletFile.Aes128CtrKdfParams b = new WalletFile.Aes128CtrKdfParams(); + b.setDklen(32); + b.setC(262144); + b.setPrf("hmac-sha256"); + b.setSalt("salt"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + b.setDklen(64); + assertNotEquals(a, b); + b.setDklen(32); + + b.setC(1); + assertNotEquals(a, b); + b.setC(262144); + + b.setPrf("other"); + assertNotEquals(a, b); + b.setPrf("hmac-sha256"); + + b.setSalt("other"); + assertNotEquals(a, b); + b.setSalt("salt"); + + // null fields + WalletFile.Aes128CtrKdfParams x = new WalletFile.Aes128CtrKdfParams(); + WalletFile.Aes128CtrKdfParams y = new WalletFile.Aes128CtrKdfParams(); + assertEquals(x, y); + x.setPrf("x"); + assertNotEquals(x, y); + } + + @Test + public void testScryptKdfParamsAllAccessors() { + WalletFile.ScryptKdfParams p = new WalletFile.ScryptKdfParams(); + p.setDklen(32); + p.setN(262144); + p.setP(1); + p.setR(8); + p.setSalt("saltvalue"); + + assertEquals(32, p.getDklen()); + assertEquals(262144, p.getN()); + assertEquals(1, p.getP()); + assertEquals(8, p.getR()); + assertEquals("saltvalue", p.getSalt()); + } + + @Test + public void testScryptKdfParamsEquals() { + WalletFile.ScryptKdfParams a = new WalletFile.ScryptKdfParams(); + a.setDklen(32); + a.setN(262144); + a.setP(1); + a.setR(8); + a.setSalt("salt"); + + WalletFile.ScryptKdfParams b = new WalletFile.ScryptKdfParams(); + b.setDklen(32); + b.setN(262144); + b.setP(1); + b.setR(8); + b.setSalt("salt"); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertTrue(a.equals(a)); + assertFalse(a.equals(null)); + assertFalse(a.equals("string")); + + b.setDklen(64); + assertNotEquals(a, b); + b.setDklen(32); + + b.setN(1); + assertNotEquals(a, b); + b.setN(262144); + + b.setP(2); + assertNotEquals(a, b); + b.setP(1); + + b.setR(16); + assertNotEquals(a, b); + b.setR(8); + + b.setSalt("other"); + assertNotEquals(a, b); + + // null salt + WalletFile.ScryptKdfParams x = new WalletFile.ScryptKdfParams(); + WalletFile.ScryptKdfParams y = new WalletFile.ScryptKdfParams(); + assertEquals(x, y); + x.setSalt("x"); + assertNotEquals(x, y); + } + + @Test + public void testJsonDeserializeWithScryptKdf() throws Exception { + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"id\":\"uuid\"," + + "\"crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"ciphertext\":\"ct\"," + + " \"cipherparams\":{\"iv\":\"iv\"}," + + " \"kdf\":\"scrypt\"," + + " \"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"salt\"}," + + " \"mac\":\"mac\"" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertEquals("TAddr", wf.getAddress()); + assertEquals(3, wf.getVersion()); + assertNotNull(wf.getCrypto()); + assertNotNull(wf.getCrypto().getKdfparams()); + assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.ScryptKdfParams); + } + + @Test + public void testJsonDeserializeWithAes128Kdf() throws Exception { + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"ciphertext\":\"ct\"," + + " \"cipherparams\":{\"iv\":\"iv\"}," + + " \"kdf\":\"pbkdf2\"," + + " \"kdfparams\":{\"dklen\":32,\"c\":262144,\"prf\":\"hmac-sha256\",\"salt\":\"salt\"}," + + " \"mac\":\"mac\"" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertNotNull(wf.getCrypto().getKdfparams()); + assertTrue(wf.getCrypto().getKdfparams() instanceof WalletFile.Aes128CtrKdfParams); + } + + @Test + public void testJsonDeserializeCryptoV1Field() throws Exception { + // Legacy files may use "Crypto" instead of "crypto" + String json = "{" + + "\"address\":\"TAddr\"," + + "\"version\":3," + + "\"Crypto\":{" + + " \"cipher\":\"aes-128-ctr\"," + + " \"kdf\":\"scrypt\"," + + " \"kdfparams\":{\"dklen\":32,\"n\":1,\"p\":1,\"r\":8,\"salt\":\"s\"}" + + "}}"; + + WalletFile wf = new ObjectMapper().readValue(json, WalletFile.class); + assertNotNull(wf.getCrypto()); + assertEquals("aes-128-ctr", wf.getCrypto().getCipher()); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java b/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java new file mode 100644 index 00000000000..3028d2a7799 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletPropertyTest.java @@ -0,0 +1,77 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertArrayEquals; + +import java.security.SecureRandom; +import org.junit.Test; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; + +/** + * Property-based roundtrip tests: decrypt(encrypt(privateKey, password)) == privateKey. + * Uses randomized inputs via loop instead of jqwik to avoid dependency verification overhead. + */ +public class WalletPropertyTest { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String CHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + @Test + public void encryptDecryptRoundtripLight() throws Exception { + for (int i = 0; i < 100; i++) { + String password = randomPassword(6, 32); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + + WalletFile walletFile = Wallet.createLight(password, keyPair); + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + + assertArrayEquals("Roundtrip failed at iteration " + i, + originalKey, recovered.getPrivateKey()); + } + } + + @Test(timeout = 120000) + public void encryptDecryptRoundtripStandard() throws Exception { + // Fewer iterations for standard scrypt (slow, ~10s each) + for (int i = 0; i < 2; i++) { + String password = randomPassword(6, 16); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + + WalletFile walletFile = Wallet.createStandard(password, keyPair); + SignInterface recovered = Wallet.decrypt(password, walletFile, true); + + assertArrayEquals("Standard roundtrip failed at iteration " + i, + originalKey, recovered.getPrivateKey()); + } + } + + @Test + public void wrongPasswordFailsDecrypt() throws Exception { + for (int i = 0; i < 50; i++) { + String password = randomPassword(6, 16); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + WalletFile walletFile = Wallet.createLight(password, keyPair); + + try { + Wallet.decrypt(password + "X", walletFile, true); + throw new AssertionError("Expected CipherException at iteration " + i); + } catch (CipherException e) { + // Expected + } + } + } + + private String randomPassword(int minLen, int maxLen) { + int len = minLen + RANDOM.nextInt(maxLen - minLen + 1); + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length()))); + } + return sb.toString(); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java new file mode 100644 index 00000000000..64752b9ca49 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java @@ -0,0 +1,167 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies that {@link WalletUtils#inputPassword()} preserves the full + * password including internal whitespace on the non-TTY (stdin) path, + * and that {@link WalletUtils#stripPasswordLine(String)} handles all + * edge cases correctly. + * + *

Previously the non-TTY path applied {@code trim() + split("\\s+")[0]} + * which silently truncated passphrases like "correct horse battery staple" + * to "correct" when piped via stdin. This test locks in the fix. + */ +public class WalletUtilsInputPasswordTest { + + private InputStream originalIn; + + @Before + public void saveStdin() { + originalIn = System.in; + // Clear the cached Scanner so each test binds to its own System.in + WalletUtils.resetSharedStdinScanner(); + } + + @After + public void restoreStdin() { + System.setIn(originalIn); + WalletUtils.resetSharedStdinScanner(); + } + + // ---------- inputPassword() behavioral tests ---------- + + @Test(timeout = 5000) + public void testInputPasswordPreservesInternalWhitespace() { + System.setIn(new ByteArrayInputStream( + "correct horse battery staple\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Password with internal whitespace must be preserved intact", + "correct horse battery staple", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordPreservesTabs() { + System.setIn(new ByteArrayInputStream( + "pass\tw0rd\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Internal tabs must be preserved", "pass\tw0rd", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordStripsTrailingCr() { + // Windows line endings + System.setIn(new ByteArrayInputStream( + "password123\r\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Trailing \\r must be stripped", "password123", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordStripsBom() { + System.setIn(new ByteArrayInputStream( + "\uFEFFpassword123\n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("UTF-8 BOM must be stripped from the start", "password123", pw); + } + + @Test(timeout = 5000) + public void testInputPasswordPreservesLeadingAndTrailingSpaces() { + // The legacy bug also called trim(); post-fix, spaces at the edges + // are part of the password. Callers that want to trim should do so + // themselves with full knowledge. + System.setIn(new ByteArrayInputStream( + " with spaces \n".getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword(); + + assertEquals("Leading and trailing spaces are part of the password", + " with spaces ", pw); + } + + @Test(timeout = 10000) + public void testInputPassword2TwicePipedPreservesInternalWhitespace() { + // M1: verifies the double-read path (inputPassword2Twice → inputPassword() + // called twice) works correctly when both lines arrive on the same + // piped stdin. Guards against regressions from Scanner lifecycle issues + // where a newly-constructed Scanner could miss bytes buffered by an + // earlier Scanner on the same InputStream. + System.setIn(new ByteArrayInputStream( + ("correct horse battery staple\n" + + "correct horse battery staple\n").getBytes(StandardCharsets.UTF_8))); + + String pw = WalletUtils.inputPassword2Twice(); + + assertEquals("Full passphrase must survive the double-read path", + "correct horse battery staple", pw); + } + + // ---------- stripPasswordLine() direct unit tests (M3) ---------- + + @Test + public void testStripPasswordLineNull() { + assertNull(WalletUtils.stripPasswordLine(null)); + } + + @Test + public void testStripPasswordLineEmpty() { + assertEquals("", WalletUtils.stripPasswordLine("")); + } + + @Test + public void testStripPasswordLineOnlyBom() { + assertEquals("", WalletUtils.stripPasswordLine("\uFEFF")); + } + + @Test + public void testStripPasswordLineOnlyLineTerminators() { + assertEquals("", WalletUtils.stripPasswordLine("\r\n\r\n")); + } + + @Test + public void testStripPasswordLineBomThenTerminator() { + assertEquals("", WalletUtils.stripPasswordLine("\uFEFF\r\n")); + } + + @Test + public void testStripPasswordLineBomAndInternalWhitespace() { + assertEquals("with spaces", + WalletUtils.stripPasswordLine("\uFEFFwith spaces\r\n")); + } + + @Test + public void testStripPasswordLineNoChange() { + assertEquals("password", WalletUtils.stripPasswordLine("password")); + } + + @Test + public void testStripPasswordLineTrailingLf() { + assertEquals("password", WalletUtils.stripPasswordLine("password\n")); + } + + @Test + public void testStripPasswordLineTrailingCr() { + assertEquals("password", WalletUtils.stripPasswordLine("password\r")); + } + + @Test + public void testStripPasswordLineMultipleTrailing() { + assertEquals("password", WalletUtils.stripPasswordLine("password\r\n\r\n")); + } +} diff --git a/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java b/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java new file mode 100644 index 00000000000..f273d751961 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletUtilsWriteTest.java @@ -0,0 +1,204 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; + +/** + * Verifies that {@link WalletUtils#generateWalletFile} and + * {@link WalletUtils#writeWalletFile} produce keystore files with + * owner-only permissions (0600) atomically, leaving no temp files behind. + * + *

Tests use light scrypt (useFullScrypt=false) where possible because + * they validate filesystem behavior, not the KDF parameters. + */ +public class WalletUtilsWriteTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private static WalletFile lightWalletFile(String password) throws Exception { + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + return Wallet.createLight(password, keyPair); + } + + @Test + public void testGenerateWalletFileCreatesOwnerOnlyFile() throws Exception { + Assume.assumeTrue("POSIX permissions test", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("gen-perms"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + File created = new File(dir, fileName); + assertTrue(created.exists()); + + Set perms = Files.getPosixFilePermissions(created.toPath()); + assertEquals("Keystore must have owner-only permissions (rw-------)", + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testGenerateWalletFileLeavesNoTempFile() throws Exception { + File dir = tempFolder.newFolder("gen-no-temp"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("No temp files should remain after generation", 0, tempFiles.length); + } + + @Test + public void testGenerateWalletFileLightScrypt() throws Exception { + File dir = tempFolder.newFolder("gen-light"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + assertNotNull(fileName); + assertTrue(fileName.endsWith(".json")); + assertTrue(new File(dir, fileName).exists()); + } + + @Test + public void testWriteWalletFileOwnerOnly() throws Exception { + Assume.assumeTrue("POSIX permissions test", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("write-perms"); + WalletFile wf = lightWalletFile("password123"); + File destination = new File(dir, "out.json"); + + WalletUtils.writeWalletFile(wf, destination); + + assertTrue(destination.exists()); + Set perms = Files.getPosixFilePermissions(destination.toPath()); + assertEquals("Keystore must have owner-only permissions (rw-------)", + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testWriteWalletFileReplacesExisting() throws Exception { + File dir = tempFolder.newFolder("write-replace"); + WalletFile wf1 = lightWalletFile("password123"); + WalletFile wf2 = lightWalletFile("password123"); + File destination = new File(dir, "out.json"); + + WalletUtils.writeWalletFile(wf1, destination); + WalletUtils.writeWalletFile(wf2, destination); + + assertTrue("Destination exists after replace", destination.exists()); + WalletFile reread = new com.fasterxml.jackson.databind.ObjectMapper() + .readValue(destination, WalletFile.class); + assertEquals("Replaced file should have wf2's address", + wf2.getAddress(), reread.getAddress()); + } + + @Test + public void testWriteWalletFileLeavesNoTempFile() throws Exception { + File dir = tempFolder.newFolder("write-no-temp"); + WalletFile wf = lightWalletFile("password123"); + File destination = new File(dir, "final.json"); + + WalletUtils.writeWalletFile(wf, destination); + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("No temp files should remain", 0, tempFiles.length); + } + + @Test + public void testWriteWalletFileCreatesParentDirectories() throws Exception { + File base = tempFolder.newFolder("write-nested"); + File destination = new File(base, "a/b/c/out.json"); + assertFalse("Parent dir does not exist yet", destination.getParentFile().exists()); + + WalletFile wf = lightWalletFile("password123"); + WalletUtils.writeWalletFile(wf, destination); + + assertTrue("Destination written", destination.exists()); + } + + @Test + public void testWriteWalletFileCleansUpTempOnFailure() throws Exception { + // Force failure by making the destination a directory — Files.move will fail + // because the source is a file. The temp file must be cleaned up. + File dir = tempFolder.newFolder("write-fail"); + File destinationAsDir = new File(dir, "blocking-dir"); + assertTrue("Setup: blocking dir created", destinationAsDir.mkdir()); + // Put a file inside so Files.move with REPLACE_EXISTING fails (non-empty dir). + assertTrue("Setup: block file", new File(destinationAsDir, "blocker").createNewFile()); + + WalletFile wf = lightWalletFile("password123"); + + try { + WalletUtils.writeWalletFile(wf, destinationAsDir); + fail("Expected IOException because destination is a non-empty directory"); + } catch (IOException expected) { + // Expected + } + + File[] tempFiles = dir.listFiles((d, name) -> name.startsWith("keystore-") + && name.endsWith(".tmp")); + assertNotNull(tempFiles); + assertEquals("Temp file must be cleaned up on failure", 0, tempFiles.length); + } + + // ---------- loadCredentials symlink behavior ---------- + + @Test + public void testLoadCredentialsFollowsSymlinkButWarns() throws Exception { + Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File realDir = tempFolder.newFolder("load-symlink-target"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + String realName = WalletUtils.generateWalletFile("password123", keyPair, realDir, false); + File realKeystore = new File(realDir, realName); + + File linkDir = tempFolder.newFolder("load-symlink-link"); + File symlink = new File(linkDir, "witness.json"); + Files.createSymbolicLink(symlink.toPath(), realKeystore.toPath()); + + // Should NOT throw — Lighthouse-style: follow the symlink, log a warning + // for the operator. Hard-rejecting would silently break legitimate SR + // deployments that organize keystores via symlinks. + Credentials creds = + WalletUtils.loadCredentials("password123", symlink, true); + assertNotNull(creds.getAddress()); + } + + @Test + public void testLoadCredentialsAcceptsRegularFile() throws Exception { + File dir = tempFolder.newFolder("load-ok"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + String fileName = WalletUtils.generateWalletFile("password123", keyPair, dir, false); + + Credentials creds = + WalletUtils.loadCredentials("password123", new File(dir, fileName), true); + assertNotNull(creds.getAddress()); + } +} diff --git a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java deleted file mode 100644 index 2642129e00a..00000000000 --- a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.tron.keystroe; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; -import org.tron.common.crypto.SignInterface; -import org.tron.keystore.Credentials; - -public class CredentialsTest { - - @Test - public void test_equality() { - Object aObject = new Object(); - SignInterface si = Mockito.mock(SignInterface.class); - SignInterface si2 = Mockito.mock(SignInterface.class); - SignInterface si3 = Mockito.mock(SignInterface.class); - byte[] address = "TQhZ7W1RudxFdzJMw6FvMnujPxrS6sFfmj".getBytes(); - byte[] address2 = "TNCmcTdyrYKMtmE1KU2itzeCX76jGm5Not".getBytes(); - Mockito.when(si.getAddress()).thenReturn(address); - Mockito.when(si2.getAddress()).thenReturn(address); - Mockito.when(si3.getAddress()).thenReturn(address2); - Credentials aCredential = Credentials.create(si); - Assert.assertFalse(aObject.equals(aCredential)); - Assert.assertFalse(aCredential.equals(aObject)); - Assert.assertFalse(aCredential.equals(null)); - Credentials anotherCredential = Credentials.create(si); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential2 = Credentials.create(si2); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential3 = Credentials.create(si3); - Assert.assertFalse(aCredential.equals(aCredential3)); - } -} diff --git a/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java new file mode 100644 index 00000000000..860980d21e5 --- /dev/null +++ b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java @@ -0,0 +1,147 @@ +package org.tron.program; + +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +/** + * Verifies the deprecated --keystore-factory CLI. + */ +public class KeystoreFactoryDeprecationTest { + + private PrintStream originalOut; + private PrintStream originalErr; + private InputStream originalIn; + + @Before + public void setup() { + originalOut = System.out; + originalErr = System.err; + originalIn = System.in; + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + } + + @After + public void teardown() throws Exception { + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + Args.clearParam(); + // Clean up Wallet dir + File wallet = new File("Wallet"); + if (wallet.exists()) { + if (wallet.isDirectory() && wallet.listFiles() != null) { + for (File f : wallet.listFiles()) { + f.delete(); + } + } + wallet.delete(); + } + } + + @Test(timeout = 10000) + public void testDeprecationWarningPrinted() throws Exception { + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errContent)); + System.setIn(new ByteArrayInputStream("exit\n".getBytes())); + + KeystoreFactory.start(); + + String errOutput = errContent.toString("UTF-8"); + assertTrue("Should contain deprecation warning", + errOutput.contains("--keystore-factory is deprecated")); + assertTrue("Should point to Toolkit.jar", + errOutput.contains("Toolkit.jar keystore")); + } + + @Test(timeout = 10000) + public void testHelpCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("help\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should show legacy commands", out.contains("GenKeystore")); + assertTrue("Should show ImportPrivateKey", out.contains("ImportPrivateKey")); + } + + @Test(timeout = 10000) + public void testInvalidCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("badcommand\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should report invalid cmd", + out.contains("Invalid cmd: badcommand")); + } + + @Test(timeout = 10000) + public void testEmptyLineSkipped() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("\n\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Should exit cleanly", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testQuitCommand() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("quit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("Quit should terminate", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testGenKeystoreTriggersError() throws Exception { + // genkeystore reads password via a nested Scanner, which conflicts + // with the outer Scanner and throws "No line found". The error is + // caught and logged, and the REPL continues. + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("genKeystore should prompt for password", + out.contains("Please input password")); + assertTrue("REPL should continue to exit", out.contains("Exit")); + } + + @Test(timeout = 10000) + public void testImportPrivateKeyTriggersPrompt() throws Exception { + // importprivatekey reads via nested Scanner — same limitation as above, + // but we at least hit the dispatch logic. + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setIn(new ByteArrayInputStream("importprivatekey\nexit\n".getBytes())); + + KeystoreFactory.start(); + + String out = outContent.toString("UTF-8"); + assertTrue("importprivatekey should prompt for key", + out.contains("Please input private key")); + } +} diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java index 7d94f813b80..f5b525cd445 100755 --- a/framework/src/test/java/org/tron/program/SolidityNodeTest.java +++ b/framework/src/test/java/org/tron/program/SolidityNodeTest.java @@ -1,8 +1,16 @@ package org.tron.program; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -10,9 +18,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; +import org.mockito.InOrder; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.application.Application; import org.tron.common.client.DatabaseGrpcClient; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.PublicMethod; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; @@ -89,4 +100,94 @@ public void testSolidityNodeHttpApiService() { solidityNodeHttpApiService.stop(); Assert.assertTrue(true); } + + @Test + public void testAwaitShutdownAlwaysStopsNode() { + Application app = mock(Application.class); + SolidityNode node = mock(SolidityNode.class); + + SolidityNode.awaitShutdown(app, node); + + InOrder inOrder = inOrder(app, node); + inOrder.verify(app).blockUntilShutdown(); + inOrder.verify(node).shutdown(); + } + + @Test + public void testAwaitShutdownStopsNodeWhenBlockedCallFails() { + Application app = mock(Application.class); + SolidityNode node = mock(SolidityNode.class); + RuntimeException expected = new RuntimeException("boom"); + doThrow(expected).when(app).blockUntilShutdown(); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> SolidityNode.awaitShutdown(app, node)); + assertSame(expected, thrown); + + InOrder inOrder = inOrder(app, node); + inOrder.verify(app).blockUntilShutdown(); + inOrder.verify(node).shutdown(); + } + + @Test + public void testShutdownSetsFlagAndShutsDownExecutors() throws Exception { + SolidityNode node = mock(SolidityNode.class); + doCallRealMethod().when(node).shutdown(); + + ExecutorService es1 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-get"); + ExecutorService es2 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-process"); + + Field flagField = SolidityNode.class.getDeclaredField("flag"); + flagField.setAccessible(true); + flagField.set(node, true); + + Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); + getBlockEsField.setAccessible(true); + getBlockEsField.set(node, es1); + + Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); + processBlockEsField.setAccessible(true); + processBlockEsField.set(node, es2); + + node.shutdown(); + + Assert.assertFalse((boolean) flagField.get(node)); + Assert.assertTrue(es1.isShutdown()); + Assert.assertTrue(es2.isShutdown()); + } + + @Test + public void testRunInitializesNamedExecutors() throws Exception { + rpcApiService.start(); + String originalAddr = Args.getInstance().getTrustNodeAddr(); + Args.getInstance().setTrustNodeAddr("127.0.0.1:" + rpcPort); + try { + SolidityNode node = new SolidityNode(dbManager); + + Field flagField = SolidityNode.class.getDeclaredField("flag"); + flagField.setAccessible(true); + flagField.set(node, false); + + Method runMethod = SolidityNode.class.getDeclaredMethod("run"); + runMethod.setAccessible(true); + runMethod.invoke(node); + + Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); + getBlockEsField.setAccessible(true); + Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); + processBlockEsField.setAccessible(true); + + ExecutorService getBlockEs = (ExecutorService) getBlockEsField.get(node); + ExecutorService processBlockEs = (ExecutorService) processBlockEsField.get(node); + + Assert.assertNotNull(getBlockEs); + Assert.assertNotNull(processBlockEs); + + ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "test-solid-get"); + ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "test-solid-process"); + } finally { + Args.getInstance().setTrustNodeAddr(originalAddr); + rpcApiService.stop(); + } + } } diff --git a/framework/src/test/java/org/tron/program/SupplementTest.java b/framework/src/test/java/org/tron/program/SupplementTest.java index 38a1b8426dd..483922cf8c5 100644 --- a/framework/src/test/java/org/tron/program/SupplementTest.java +++ b/framework/src/test/java/org/tron/program/SupplementTest.java @@ -27,7 +27,6 @@ import org.tron.core.config.args.Args; import org.tron.core.services.http.HttpSelfFormatFieldName; import org.tron.core.store.StorageRowStore; -import org.tron.keystore.WalletUtils; public class SupplementTest extends BaseTest { @@ -54,12 +53,6 @@ public void testGet() throws Exception { String p = dbPath + File.separator; dbBackupConfig.initArgs(true, p + "propPath", p + "bak1path/", p + "bak2path/", 1); - WalletUtils.generateFullNewWalletFile("123456", new File(dbPath)); - WalletUtils.generateLightNewWalletFile("123456", new File(dbPath)); - WalletUtils.getDefaultKeyDirectory(); - WalletUtils.getTestnetKeyDirectory(); - WalletUtils.getMainnetKeyDirectory(); - Value value = new Value(new byte[]{1}); value.asBytes(); value = new Value(1); diff --git a/framework/src/test/resources/config-localtest.conf b/framework/src/test/resources/config-localtest.conf index f1f40dead76..53a78d3e4c6 100644 --- a/framework/src/test/resources/config-localtest.conf +++ b/framework/src/test/resources/config-localtest.conf @@ -57,7 +57,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node.backup { diff --git a/framework/src/test/resources/config-test-dbbackup.conf b/framework/src/test/resources/config-test-dbbackup.conf index 44dd0164b2d..b660965f3e9 100644 --- a/framework/src/test/resources/config-test-dbbackup.conf +++ b/framework/src/test/resources/config-test-dbbackup.conf @@ -60,7 +60,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node.backup { diff --git a/framework/src/test/resources/config-test-index.conf b/framework/src/test/resources/config-test-index.conf index b41fdfe8505..72e4a04f612 100644 --- a/framework/src/test/resources/config-test-index.conf +++ b/framework/src/test/resources/config-test-index.conf @@ -54,7 +54,7 @@ storage { node.discovery = { enable = true persist = true - external.ip = null + external.ip = "" } node { diff --git a/plugins/README.md b/plugins/README.md index db25811882f..b6540beef1a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -143,3 +143,78 @@ NOTE: large db may GC overhead limit exceeded. - ``: Source path for database. Default: output-directory/database - `--db`: db name. - `-h | --help`: provide the help info + +## Keystore + +Keystore provides commands for managing account keystore files (Web3 Secret Storage format). + +> **Migrating from `--keystore-factory`**: The legacy `FullNode.jar --keystore-factory` interactive mode is deprecated. Use the Toolkit keystore commands below instead. The mapping is: +> - `GenKeystore` → `keystore new` +> - `ImportPrivateKey` → `keystore import` +> - (new) `keystore list` — list all keystores in a directory +> - (new) `keystore update` — change the password of a keystore + +### Subcommands + +#### keystore new + +Generate a new keystore file with a random keypair. + +```shell script +# full command + java -jar Toolkit.jar keystore new [-h] [--keystore-dir=

] [--password-file=] [--sm2] [--json] +# examples + java -jar Toolkit.jar keystore new # interactive prompt + java -jar Toolkit.jar keystore new --keystore-dir /data/keystores # custom directory + java -jar Toolkit.jar keystore new --password-file pass.txt --json # non-interactive with JSON output +``` + +#### keystore import + +Import a private key into a new keystore file. + +```shell script +# full command + java -jar Toolkit.jar keystore import [-h] [--keystore-dir=] [--password-file=] [--key-file=] [--sm2] [--force] [--json] +# examples + java -jar Toolkit.jar keystore import # interactive prompt + java -jar Toolkit.jar keystore import --key-file key.txt --json # from file with JSON output +``` + +#### keystore list + +List all keystore files in a directory. + +```shell script +# full command + java -jar Toolkit.jar keystore list [-h] [--keystore-dir=] [--json] +# examples + java -jar Toolkit.jar keystore list # list default ./Wallet directory + java -jar Toolkit.jar keystore list --keystore-dir /data/keystores # custom directory +``` + +> **Note**: `list` displays the `address` field as declared in each keystore JSON without decrypting the file. A tampered keystore can claim an address that does not correspond to its encrypted private key. The address is only cryptographically verified at decryption time (e.g. by `update` or by tools that load the credentials). Only trust keystores from sources you control. + +#### keystore update + +Change the password of a keystore file. + +```shell script +# full command + java -jar Toolkit.jar keystore update [-h]
[--keystore-dir=] [--password-file=] [--sm2] [--json] +# examples + java -jar Toolkit.jar keystore update TXyz...abc # interactive prompt + java -jar Toolkit.jar keystore update TXyz...abc --keystore-dir /data/ks # custom directory +``` + +When using `--password-file` with `update`, the file must contain exactly two lines: the **current** password on the first line and the **new** password on the second line. Both leading/trailing whitespace within a line is preserved (passphrases with spaces are supported). + +### Common Options + +- `--keystore-dir`: Keystore directory, default: `./Wallet`. +- `--password-file`: Read password from a file instead of interactive prompt. For `keystore update`, the file must contain exactly two lines (current password, then new password). +- `--key-file`: Read the private key (hex, with or without `0x` prefix) from a file instead of the interactive prompt (`keystore import` only). +- `--force`: For `keystore import`, allow importing a private key whose address already has a keystore in the directory (creates an additional file). +- `--sm2`: Use SM2 algorithm instead of ECDSA (for `new` and `import`). +- `--json`: Output in JSON format for scripting. +- `-h | --help`: Provide the help info. diff --git a/plugins/build.gradle b/plugins/build.gradle index 85dcdd2342d..2e358a884a3 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -34,6 +34,12 @@ dependencies { implementation fileTree(dir: 'libs', include: '*.jar') testImplementation project(":framework") testImplementation project(":framework").sourceSets.test.output + implementation(project(":crypto")) { + exclude group: 'io.github.tronprotocol', module: 'libp2p' + exclude group: 'io.prometheus' + exclude group: 'org.aspectj' + exclude group: 'org.apache.httpcomponents' + } implementation group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' implementation group: 'me.tongfei', name: 'progressbar', version: '0.9.3' diff --git a/plugins/src/main/java/common/org/tron/plugins/Keystore.java b/plugins/src/main/java/common/org/tron/plugins/Keystore.java new file mode 100644 index 00000000000..6929bb406ea --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/Keystore.java @@ -0,0 +1,19 @@ +package org.tron.plugins; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "keystore", + mixinStandardHelpOptions = true, + version = "keystore command 1.0", + description = "Manage keystore files for account keys.", + subcommands = {CommandLine.HelpCommand.class, + KeystoreNew.class, + KeystoreImport.class, + KeystoreList.class, + KeystoreUpdate.class + }, + commandListHeading = "%nCommands:%n%nThe most commonly used keystore commands are:%n" +) +public class Keystore { +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java new file mode 100644 index 00000000000..6959a7f8177 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java @@ -0,0 +1,304 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; + +/** + * Shared utilities for keystore CLI commands. + */ +final class KeystoreCliUtils { + + private static final long MAX_FILE_SIZE = 1024; + + /** + * Cap on the size of a single keystore JSON read during directory scans. + * Standard V3 keystores are ~500–700 bytes; 8 KiB leaves headroom for + * unusual scrypt parameter combinations while bounding the memory cost + * of scanning a hostile directory of planted oversized files. + */ + static final long MAX_KEYSTORE_SIZE = 8 * 1024; + + private KeystoreCliUtils() { + } + + /** + * Read a regular file safely without following symbolic links. + * + *

This prevents an attacker who can plant files in a user-supplied + * path from redirecting the read to an arbitrary file on disk (e.g. a + * symlink pointing at {@code /etc/shadow} or a user's SSH private key). + * Also rejects FIFOs, devices and other non-regular files. + * + * @param file the file to read + * @param maxSize maximum acceptable file size in bytes + * @param label human-readable label used in error messages + * @param err writer for diagnostic messages + * @return file bytes, or {@code null} if the file is missing, a symlink, + * not a regular file, or too large (err is written in each case) + */ + static byte[] readRegularFile(File file, long maxSize, String label, PrintWriter err) + throws IOException { + Path path = file.toPath(); + + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } catch (NoSuchFileException e) { + err.println(label + " not found: " + file.getPath()); + return null; + } + + if (attrs.isSymbolicLink()) { + err.println("Refusing to follow symbolic link: " + file.getPath()); + return null; + } + if (!attrs.isRegularFile()) { + err.println("Not a regular file: " + file.getPath()); + return null; + } + if (attrs.size() > maxSize) { + err.println(label + " too large (max " + maxSize + " bytes): " + file.getPath()); + return null; + } + + int size = (int) attrs.size(); + java.util.Set openOptions = new HashSet<>(); + openOptions.add(StandardOpenOption.READ); + openOptions.add(LinkOption.NOFOLLOW_LINKS); + try (SeekableByteChannel ch = Files.newByteChannel(path, openOptions)) { + ByteBuffer buf = ByteBuffer.allocate(size); + while (buf.hasRemaining()) { + if (ch.read(buf) < 0) { + break; + } + } + if (buf.position() < size) { + byte[] actual = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, actual, 0, buf.position()); + return actual; + } + return buf.array(); + } + } + + static String readPassword(File passwordFile, PrintWriter err) throws IOException { + if (passwordFile != null) { + byte[] bytes = readRegularFile(passwordFile, MAX_FILE_SIZE, "Password file", err); + if (bytes == null) { + return null; + } + try { + String password = WalletUtils.stripPasswordLine( + new String(bytes, StandardCharsets.UTF_8)); + // Reject multi-line password files. stripPasswordLine only trims + // trailing terminators; any remaining \n/\r means the file had + // interior line breaks. A common mistake is passing a two-line + // `keystore update` password file to `keystore new` / `import` — + // without this guard the literal "old\nnew" would silently become + // the password, and neither visible line alone would unlock the + // keystore later. + if (password.indexOf('\n') >= 0 || password.indexOf('\r') >= 0) { + err.println("Password file contains multiple lines; provide a " + + "single-line password (the `keystore update` two-line " + + "format is not accepted here)."); + return null; + } + if (!WalletUtils.passwordValid(password)) { + err.println("Invalid password: must be at least 6 characters."); + return null; + } + return password; + } finally { + Arrays.fill(bytes, (byte) 0); + } + } + + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --password-file to provide password."); + return null; + } + + char[] pwd1 = console.readPassword("Enter password: "); + if (pwd1 == null) { + err.println("Password input cancelled."); + return null; + } + char[] pwd2 = console.readPassword("Confirm password: "); + if (pwd2 == null) { + Arrays.fill(pwd1, '\0'); + err.println("Password input cancelled."); + return null; + } + try { + if (!Arrays.equals(pwd1, pwd2)) { + err.println("Passwords do not match."); + return null; + } + String password = new String(pwd1); + if (!WalletUtils.passwordValid(password)) { + err.println("Invalid password: must be at least 6 characters."); + return null; + } + return password; + } finally { + Arrays.fill(pwd1, '\0'); + Arrays.fill(pwd2, '\0'); + } + } + + static void ensureDirectory(File dir) throws IOException { + Path path = dir.toPath(); + if (Files.exists(path) && !Files.isDirectory(path)) { + throw new IOException( + "Path exists but is not a directory: " + dir.getAbsolutePath()); + } + Files.createDirectories(path); + } + + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure( + com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + static ObjectMapper mapper() { + return MAPPER; + } + + static void printJson(PrintWriter out, PrintWriter err, Map fields) { + try { + out.println(MAPPER.writeValueAsString(fields)); + } catch (Exception e) { + err.println("Error writing JSON output"); + } + } + + static Map jsonMap(String... keyValues) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length - 1; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + static boolean checkFileExists(File file, String label, PrintWriter err) { + if (file != null && !file.exists()) { + err.println(label + " not found: " + file.getPath()); + return false; + } + return true; + } + + /** + * Read the bytes of a keystore-directory entry, refusing to follow + * symbolic links and rejecting non-regular files. Returns {@code null} + * (with a warning to {@code err}) when the entry should be skipped. + * + *

Unlike {@code Files.readAttributes(...) + MAPPER.readValue(file, ...)}, + * this opens the channel with {@link LinkOption#NOFOLLOW_LINKS} so the + * {@code O_NOFOLLOW} flag is enforced atomically by the kernel at + * {@code open(2)} — closing the TOCTOU window between an lstat-style + * check and a follow-symlink {@code FileInputStream} open. The caller + * then deserializes the bytes via {@code ObjectMapper.readValue(byte[], + * Class)}. + * + *

Files larger than {@link #MAX_KEYSTORE_SIZE} are skipped to bound + * memory cost when scanning a hostile or oversized directory. + */ + static byte[] readKeystoreFile(File file, PrintWriter err) { + Path path = file.toPath(); + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + return null; + } + if (attrs.isSymbolicLink()) { + err.println("Warning: skipping symbolic link: " + file.getName()); + return null; + } + if (!attrs.isRegularFile()) { + err.println("Warning: skipping non-regular file: " + file.getName()); + return null; + } + if (attrs.size() > MAX_KEYSTORE_SIZE) { + err.println("Warning: skipping oversized file (>" + MAX_KEYSTORE_SIZE + + " bytes): " + file.getName()); + return null; + } + + int size = (int) attrs.size(); + java.util.Set openOptions = new HashSet<>(); + openOptions.add(StandardOpenOption.READ); + openOptions.add(LinkOption.NOFOLLOW_LINKS); + try (SeekableByteChannel ch = Files.newByteChannel(path, openOptions)) { + ByteBuffer buf = ByteBuffer.allocate(size); + while (buf.hasRemaining()) { + if (ch.read(buf) < 0) { + break; + } + } + if (buf.position() < size) { + byte[] actual = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, actual, 0, buf.position()); + return actual; + } + return buf.array(); + } catch (IOException e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + return null; + } + } + + static void printSecurityTips(PrintWriter out, String address, String fileName) { + out.println(); + out.println("Public address of the key: " + address); + out.println("Path of the secret key file: " + fileName); + out.println(); + out.println( + "- You can share your public address with anyone." + + " Others need it to interact with you."); + out.println( + "- You must NEVER share the secret key with anyone!" + + " The key controls access to your funds!"); + out.println( + "- You must BACKUP your key file!" + + " Without the key, it's impossible to access account funds!"); + out.println( + "- You must REMEMBER your password!" + + " Without the password, it's impossible to decrypt the key!"); + } + + /** + * Check if a WalletFile represents a decryptable V3 keystore. + * Delegates to {@link Wallet#isValidKeystoreFile(WalletFile)} so the + * discovery predicate stays in sync with decryption-time validation — + * a JSON stub with empty or unsupported cipher/KDF is rejected here + * rather than silently showing up as a "keystore" and failing later. + */ + static boolean isValidKeystoreFile(WalletFile wf) { + return org.tron.keystore.Wallet.isValidKeystoreFile(wf); + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java new file mode 100644 index 00000000000..67c8e6bc4c6 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreImport.java @@ -0,0 +1,188 @@ +package org.tron.plugins; + +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.apache.commons.lang3.StringUtils; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "import", + mixinStandardHelpOptions = true, + description = "Import a private key into a new keystore file.") +public class KeystoreImport implements Callable { + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--key-file"}, + description = "Read private key from file instead of interactive prompt") + private File keyFile; + + @Option(names = {"--password-file"}, + description = "Read password from file instead of interactive prompt") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Option(names = {"--force"}, + description = "Allow import even if address already exists") + private boolean force; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + try { + if (!KeystoreCliUtils.checkFileExists(keyFile, "Key file", err)) { + return 1; + } + KeystoreCliUtils.ensureDirectory(keystoreDir); + + String privateKey = readPrivateKey(err); + if (privateKey == null) { + return 1; + } + + if (privateKey.startsWith("0x") || privateKey.startsWith("0X")) { + privateKey = privateKey.substring(2); + } + if (!isValidPrivateKey(privateKey)) { + err.println("Invalid private key: must be 64 hex characters."); + return 1; + } + + String password = KeystoreCliUtils.readPassword(passwordFile, err); + if (password == null) { + return 1; + } + + boolean ecKey = !sm2; + SignInterface keyPair; + try { + keyPair = SignUtils.fromPrivate( + ByteArray.fromHexString(privateKey), ecKey); + } catch (Exception e) { + err.println("Invalid private key: not a valid key" + + " for the selected algorithm."); + return 1; + } + String address = Credentials.create(keyPair).getAddress(); + String existingFile = findExistingKeystore(keystoreDir, address, err); + if (existingFile != null && !force) { + err.println("Keystore for address " + address + + " already exists: " + existingFile + + ". Use --force to import anyway."); + return 1; + } + String fileName = WalletUtils.generateWalletFile( + password, keyPair, keystoreDir, true); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", address, "file", fileName)); + } else { + out.println("Imported keystore successfully"); + KeystoreCliUtils.printSecurityTips(out, address, + new File(keystoreDir, fileName).getPath()); + } + return 0; + } catch (CipherException e) { + err.println("Encryption error: " + e.getMessage()); + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } + + private String readPrivateKey(PrintWriter err) throws IOException { + if (keyFile != null) { + byte[] bytes = KeystoreCliUtils.readRegularFile(keyFile, 1024, "Key file", err); + if (bytes == null) { + return null; + } + try { + return new String(bytes, StandardCharsets.UTF_8).trim(); + } finally { + Arrays.fill(bytes, (byte) 0); + } + } + + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --key-file to provide private key."); + return null; + } + + char[] key = console.readPassword("Enter private key (hex): "); + if (key == null) { + err.println("Input cancelled."); + return null; + } + try { + return new String(key); + } finally { + Arrays.fill(key, '\0'); + } + } + + private static final java.util.regex.Pattern HEX_PATTERN = + java.util.regex.Pattern.compile("[0-9a-fA-F]{64}"); + + private boolean isValidPrivateKey(String key) { + return !StringUtils.isEmpty(key) && HEX_PATTERN.matcher(key).matches(); + } + + private String findExistingKeystore(File dir, String address, PrintWriter err) { + if (!dir.exists() || !dir.isDirectory()) { + return null; + } + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + if (files == null) { + return null; + } + com.fasterxml.jackson.databind.ObjectMapper mapper = + KeystoreCliUtils.mapper(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile wf = mapper.readValue(bytes, WalletFile.class); + if (KeystoreCliUtils.isValidKeystoreFile(wf) + && address.equals(wf.getAddress())) { + return file.getName(); + } + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + return null; + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java new file mode 100644 index 00000000000..e7218be9fbf --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreList.java @@ -0,0 +1,110 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import org.tron.keystore.WalletFile; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "list", + mixinStandardHelpOptions = true, + description = "List all keystore files in a directory.") +public class KeystoreList implements Callable { + + private static final ObjectMapper MAPPER = KeystoreCliUtils.mapper(); + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + + if (!keystoreDir.exists() || !keystoreDir.isDirectory()) { + if (json) { + return printEmptyJson(out, err); + } else { + out.println("No keystores found in: " + keystoreDir.getAbsolutePath()); + } + return 0; + } + + File[] files = keystoreDir.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null || files.length == 0) { + if (json) { + return printEmptyJson(out, err); + } else { + out.println("No keystores found in: " + keystoreDir.getAbsolutePath()); + } + return 0; + } + + List> entries = new ArrayList<>(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile walletFile = MAPPER.readValue(bytes, WalletFile.class); + if (!KeystoreCliUtils.isValidKeystoreFile(walletFile)) { + continue; + } + Map entry = new LinkedHashMap<>(); + entry.put("address", walletFile.getAddress()); + entry.put("file", file.getName()); + entries.add(entry); + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + + if (json) { + try { + Map result = new LinkedHashMap<>(); + result.put("keystores", entries); + out.println(MAPPER.writeValueAsString(result)); + } catch (Exception e) { + err.println("Error writing JSON output"); + return 1; + } + } else if (entries.isEmpty()) { + out.println("No valid keystores found in: " + keystoreDir.getAbsolutePath()); + } else { + for (Map entry : entries) { + out.printf("%-45s %s%n", entry.get("address"), entry.get("file")); + } + } + return 0; + } + + private int printEmptyJson(PrintWriter out, PrintWriter err) { + try { + Map result = new LinkedHashMap<>(); + result.put("keystores", new ArrayList<>()); + out.println(MAPPER.writeValueAsString(result)); + return 0; + } catch (Exception e) { + err.println("Error writing JSON output"); + return 1; + } + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java new file mode 100644 index 00000000000..39d2bdd3502 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreNew.java @@ -0,0 +1,77 @@ +package org.tron.plugins; + +import java.io.File; +import java.io.PrintWriter; +import java.util.concurrent.Callable; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.Utils; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +@Command(name = "new", + mixinStandardHelpOptions = true, + description = "Generate a new keystore file with a random keypair.") +public class KeystoreNew implements Callable { + + @Spec + private CommandSpec spec; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--password-file"}, + description = "Read password from file instead of interactive prompt") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + try { + KeystoreCliUtils.ensureDirectory(keystoreDir); + + String password = KeystoreCliUtils.readPassword(passwordFile, err); + if (password == null) { + return 1; + } + + boolean ecKey = !sm2; + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), ecKey); + String fileName = WalletUtils.generateWalletFile( + password, keyPair, keystoreDir, true); + + String address = Credentials.create(keyPair).getAddress(); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", address, "file", fileName)); + } else { + out.println("Your new key was generated"); + KeystoreCliUtils.printSecurityTips(out, address, + new File(keystoreDir, fileName).getPath()); + } + return 0; + } catch (CipherException e) { + err.println("Encryption error: " + e.getMessage()); + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java new file mode 100644 index 00000000000..4ef6cbbd71e --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreUpdate.java @@ -0,0 +1,245 @@ +package org.tron.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.tron.common.crypto.SignInterface; +import org.tron.core.exception.CipherException; +import org.tron.keystore.Wallet; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; + +@Command(name = "update", + mixinStandardHelpOptions = true, + description = "Change the password of a keystore file.") +public class KeystoreUpdate implements Callable { + + private static final ObjectMapper MAPPER = KeystoreCliUtils.mapper(); + private static final String INPUT_CANCELLED = "Password input cancelled."; + + @Spec + private CommandSpec spec; + + @Parameters(index = "0", description = "Address of the keystore to update") + private String address; + + @Option(names = {"--keystore-dir"}, + description = "Keystore directory (default: ./Wallet)", + defaultValue = "Wallet") + private File keystoreDir; + + @Option(names = {"--json"}, + description = "Output in JSON format") + private boolean json; + + @Option(names = {"--password-file"}, + description = "Read old and new passwords from file (one per line)") + private File passwordFile; + + @Option(names = {"--sm2"}, + description = "Use SM2 algorithm instead of ECDSA") + private boolean sm2; + + @Override + public Integer call() { + PrintWriter out = spec.commandLine().getOut(); + PrintWriter err = spec.commandLine().getErr(); + // Hoisted out of the try so the legacy-truncation hint in the catch + // block can inspect whether the user-supplied password contained + // whitespace (which is the only case truncation can explain). + String oldPassword = null; + try { + File keystoreFile = findKeystoreByAddress(address, err); + if (keystoreFile == null) { + // findKeystoreByAddress already prints the specific error + return 1; + } + + String newPassword; + + if (passwordFile != null) { + byte[] bytes = KeystoreCliUtils.readRegularFile( + passwordFile, 1024, "Password file", err); + if (bytes == null) { + return 1; + } + try { + String content = new String(bytes, StandardCharsets.UTF_8); + // Strip UTF-8 BOM if present (Windows Notepad) + if (content.length() > 0 && content.charAt(0) == '\uFEFF') { + content = content.substring(1); + } + // String.split with the default zero-limit form already drops + // trailing empty strings, so "old\nnew" and "old\nnew\n" both + // yield length 2; require strict equality so a stray third line + // (e.g. someone confusingly providing a confirm line, or the + // wrong file altogether) is reported rather than silently + // discarded. + String[] lines = content.split("\\r?\\n|\\r"); + if (lines.length != 2) { + err.println("Password file must contain exactly two lines: " + + "current password on the first line and new password " + + "on the second line (no confirmation line)."); + return 1; + } + oldPassword = WalletUtils.stripPasswordLine(lines[0]); + newPassword = WalletUtils.stripPasswordLine(lines[1]); + } finally { + Arrays.fill(bytes, (byte) 0); + } + } else { + Console console = System.console(); + if (console == null) { + err.println("No interactive terminal available. " + + "Use --password-file to provide passwords."); + return 1; + } + char[] oldPwd = console.readPassword("Enter current password: "); + if (oldPwd == null) { + err.println(INPUT_CANCELLED); + return 1; + } + char[] newPwd = console.readPassword("Enter new password: "); + if (newPwd == null) { + Arrays.fill(oldPwd, '\0'); + err.println(INPUT_CANCELLED); + return 1; + } + char[] confirmPwd = console.readPassword("Confirm new password: "); + if (confirmPwd == null) { + Arrays.fill(oldPwd, '\0'); + Arrays.fill(newPwd, '\0'); + err.println(INPUT_CANCELLED); + return 1; + } + try { + oldPassword = new String(oldPwd); + newPassword = new String(newPwd); + String confirmPassword = new String(confirmPwd); + if (!newPassword.equals(confirmPassword)) { + err.println("New passwords do not match."); + return 1; + } + } finally { + Arrays.fill(oldPwd, '\0'); + Arrays.fill(newPwd, '\0'); + Arrays.fill(confirmPwd, '\0'); + } + } + + // Skip validation on old password: keystore may predate the minimum-length policy + if (!WalletUtils.passwordValid(newPassword)) { + err.println("Invalid new password: must be at least 6 characters."); + return 1; + } + + boolean ecKey = !sm2; + // Re-read via NOFOLLOW byte channel to close the TOCTOU window between + // findKeystoreByAddress and this read — an attacker with directory + // write access could otherwise swap the file for a symlink in between. + byte[] keystoreBytes = KeystoreCliUtils.readKeystoreFile(keystoreFile, err); + if (keystoreBytes == null) { + // readKeystoreFile already printed the specific reason + return 1; + } + WalletFile walletFile = MAPPER.readValue(keystoreBytes, WalletFile.class); + SignInterface keyPair = Wallet.decrypt(oldPassword, walletFile, ecKey); + + // createStandard already sets the correctly-derived address. Do NOT override + // with walletFile.getAddress() — that would propagate a potentially spoofed + // address from the JSON. + WalletFile newWalletFile = Wallet.createStandard(newPassword, keyPair); + // writeWalletFile does a secure temp-file + atomic rename internally. + WalletUtils.writeWalletFile(newWalletFile, keystoreFile); + + // Use the derived address from newWalletFile, not walletFile.getAddress(). + // Defense-in-depth: Wallet.decrypt already rejects spoofed addresses, but + // relying on the derived value keeps this code correct even if that check + // is ever weakened. + String verifiedAddress = newWalletFile.getAddress(); + if (json) { + KeystoreCliUtils.printJson(out, err, KeystoreCliUtils.jsonMap( + "address", verifiedAddress, + "file", keystoreFile.getName(), + "status", "updated")); + } else { + out.println("Password updated for: " + verifiedAddress); + } + return 0; + } catch (CipherException e) { + err.println("Decryption failed: " + e.getMessage()); + // Legacy-truncation hint: keystores created via + // `FullNode.jar --keystore-factory` in non-TTY mode (e.g. + // `echo PASS | java ...`) were encrypted with only the first + // whitespace-separated word of the password due to a bug in the + // legacy input path. The hint only fires if the provided password + // actually contains whitespace — otherwise truncation cannot be the + // cause of the decryption failure and the hint would be noise for + // the far more common "wrong password" case. + if (oldPassword != null && oldPassword.matches(".*\\s.*")) { + err.println("Tip: if this keystore was created with " + + "`FullNode.jar --keystore-factory` in non-TTY mode, the legacy " + + "code truncated the password at the first whitespace. " + + "Try re-running with only the first whitespace-separated word " + + "of your passphrase as the current password; you can then " + + "choose the full phrase as the new password."); + } + return 1; + } catch (Exception e) { + err.println("Error: " + e.getMessage()); + return 1; + } + } + + private File findKeystoreByAddress(String targetAddress, PrintWriter err) { + if (!keystoreDir.exists() || !keystoreDir.isDirectory()) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + File[] files = keystoreDir.listFiles((dir, name) -> name.endsWith(".json")); + if (files == null) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + java.util.List matches = new java.util.ArrayList<>(); + for (File file : files) { + byte[] bytes = KeystoreCliUtils.readKeystoreFile(file, err); + if (bytes == null) { + continue; + } + try { + WalletFile wf = MAPPER.readValue(bytes, WalletFile.class); + if (KeystoreCliUtils.isValidKeystoreFile(wf) + && targetAddress.equals(wf.getAddress())) { + matches.add(file); + } + } catch (Exception e) { + err.println("Warning: skipping unreadable file: " + file.getName()); + } + } + if (matches.size() > 1) { + err.println("Multiple keystores found for address " + + targetAddress + ":"); + for (File m : matches) { + err.println(" " + m.getName()); + } + err.println("Please remove duplicates and retry."); + return null; + } + if (matches.isEmpty()) { + err.println("No keystore found for address: " + targetAddress); + return null; + } + return matches.get(0); + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/Toolkit.java b/plugins/src/main/java/common/org/tron/plugins/Toolkit.java index 3b9972de1c5..7a979fe256c 100644 --- a/plugins/src/main/java/common/org/tron/plugins/Toolkit.java +++ b/plugins/src/main/java/common/org/tron/plugins/Toolkit.java @@ -3,7 +3,7 @@ import java.util.concurrent.Callable; import picocli.CommandLine; -@CommandLine.Command(subcommands = { CommandLine.HelpCommand.class, Db.class}) +@CommandLine.Command(subcommands = { CommandLine.HelpCommand.class, Db.class, Keystore.class}) public class Toolkit implements Callable { diff --git a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java index 60db838a80d..07bddc461a0 100644 --- a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java @@ -90,7 +90,6 @@ public void clear() { public void testTools(String dbType, int checkpointVersion) throws InterruptedException, IOException { logger.info("dbType {}, checkpointVersion {}", dbType, checkpointVersion); - dbPath = String.format("%s_%s_%d", dbPath, dbType, System.currentTimeMillis()); init(dbType); final String[] argsForSnapshot = new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", @@ -166,7 +165,8 @@ private void generateSomeTransactions(int during) { try { Thread.sleep(sleepOnce); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); + return; } if ((runTime += sleepOnce) > during) { return; diff --git a/plugins/src/test/java/org/tron/plugins/DbMoveTest.java b/plugins/src/test/java/org/tron/plugins/DbMoveTest.java index 5b25739f272..ec4f0d545b0 100644 --- a/plugins/src/test/java/org/tron/plugins/DbMoveTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbMoveTest.java @@ -31,7 +31,6 @@ private void init(DbTool.DbType dbType, String path) throws IOException, RocksDB DbTool.getDB(path, ACCOUNT, dbType).close(); DbTool.getDB(path, DBUtils.MARKET_PAIR_PRICE_TO_ORDER, dbType).close(); DbTool.getDB(path, TRANS, dbType).close(); - } @After diff --git a/plugins/src/test/java/org/tron/plugins/DbTest.java b/plugins/src/test/java/org/tron/plugins/DbTest.java index bbcc1a0bbf7..d22addfbae8 100644 --- a/plugins/src/test/java/org/tron/plugins/DbTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbTest.java @@ -18,10 +18,10 @@ public class DbTest { - public String INPUT_DIRECTORY; + protected String INPUT_DIRECTORY; private static final String ACCOUNT = "account"; private static final String MARKET = DBUtils.MARKET_PAIR_PRICE_TO_ORDER; - public CommandLine cli = new CommandLine(new Toolkit()); + protected CommandLine cli = new CommandLine(new Toolkit()); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java new file mode 100644 index 00000000000..264e1cb4519 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java @@ -0,0 +1,348 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.keystore.WalletFile; + +public class KeystoreCliUtilsTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testJsonMapEven() { + Map m = KeystoreCliUtils.jsonMap("a", "1", "b", "2"); + assertEquals(2, m.size()); + assertEquals("1", m.get("a")); + assertEquals("2", m.get("b")); + } + + @Test + public void testJsonMapPreservesOrder() { + Map m = KeystoreCliUtils.jsonMap( + "z", "1", "a", "2", "m", "3"); + String[] keys = m.keySet().toArray(new String[0]); + assertEquals("z", keys[0]); + assertEquals("a", keys[1]); + assertEquals("m", keys[2]); + } + + @Test + public void testJsonMapEmpty() { + Map m = KeystoreCliUtils.jsonMap(); + assertTrue(m.isEmpty()); + } + + private static WalletFile.Crypto supportedCrypto() { + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("scrypt"); + return crypto; + } + + @Test + public void testIsValidKeystoreFileValid() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + wf.setCrypto(supportedCrypto()); + assertTrue(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileNullAddress() { + WalletFile wf = new WalletFile(); + wf.setVersion(3); + wf.setCrypto(supportedCrypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileNullCrypto() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileWrongVersion() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(2); + wf.setCrypto(supportedCrypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsEmptyCryptoStub() { + // {"address":"T...","version":3,"crypto":{}} — passes the old checks + // but Wallet.validate would later reject it. Discovery should skip it. + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + wf.setCrypto(new WalletFile.Crypto()); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsUnsupportedCipher() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("des"); + crypto.setKdf("scrypt"); + wf.setCrypto(crypto); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileRejectsUnsupportedKdf() { + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("bcrypt"); + wf.setCrypto(crypto); + assertFalse(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testIsValidKeystoreFileAcceptsPbkdf2Kdf() { + // pbkdf2 is the other supported KDF (used by some Ethereum keystores). + WalletFile wf = new WalletFile(); + wf.setAddress("TAddr"); + wf.setVersion(3); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setKdf("pbkdf2"); + wf.setCrypto(crypto); + assertTrue(KeystoreCliUtils.isValidKeystoreFile(wf)); + } + + @Test + public void testCheckFileExistsNull() { + StringWriter err = new StringWriter(); + assertTrue(KeystoreCliUtils.checkFileExists(null, "Label", + new PrintWriter(err))); + assertEquals("", err.toString()); + } + + @Test + public void testCheckFileExistsMissing() { + StringWriter err = new StringWriter(); + File missing = new File("/tmp/nonexistent-cli-utils-test-file"); + assertFalse(KeystoreCliUtils.checkFileExists(missing, "Key file", + new PrintWriter(err))); + assertTrue(err.toString().contains("Key file not found")); + } + + @Test + public void testCheckFileExistsPresent() throws Exception { + StringWriter err = new StringWriter(); + File f = tempFolder.newFile("present.txt"); + assertTrue(KeystoreCliUtils.checkFileExists(f, "Key file", + new PrintWriter(err))); + } + + @Test + public void testReadPasswordFromFile() throws Exception { + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "goodpassword".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFromFileWithLineEndings() throws Exception { + File pwFile = tempFolder.newFile("pw-crlf.txt"); + Files.write(pwFile.toPath(), "goodpassword\r\n".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFromFileWithBom() throws Exception { + File pwFile = tempFolder.newFile("pw-bom.txt"); + Files.write(pwFile.toPath(), + "\uFEFFgoodpassword".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertEquals("goodpassword", pw); + } + + @Test + public void testReadPasswordFileTooLarge() throws Exception { + File pwFile = tempFolder.newFile("pw-big.txt"); + byte[] big = new byte[1025]; + java.util.Arrays.fill(big, (byte) 'a'); + Files.write(pwFile.toPath(), big); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("too large")); + } + + @Test + public void testReadPasswordFileShort() throws Exception { + File pwFile = tempFolder.newFile("pw-short.txt"); + Files.write(pwFile.toPath(), "abc".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("at least 6")); + } + + @Test + public void testReadPasswordFileNotFound() throws Exception { + StringWriter err = new StringWriter(); + String pw = KeystoreCliUtils.readPassword( + new File("/tmp/nonexistent-pw-direct-test.txt"), new PrintWriter(err)); + assertNull(pw); + assertTrue(err.toString().contains("Password file not found")); + } + + @Test + public void testEnsureDirectoryCreatesNested() throws Exception { + File dir = new File(tempFolder.getRoot(), "a/b/c"); + assertFalse(dir.exists()); + KeystoreCliUtils.ensureDirectory(dir); + assertTrue(dir.exists()); + assertTrue(dir.isDirectory()); + } + + @Test + public void testEnsureDirectoryExisting() throws Exception { + File dir = tempFolder.newFolder("existing"); + KeystoreCliUtils.ensureDirectory(dir); + assertTrue(dir.isDirectory()); + } + + @Test(expected = java.io.IOException.class) + public void testEnsureDirectoryPathIsFile() throws Exception { + File f = tempFolder.newFile("not-a-dir"); + KeystoreCliUtils.ensureDirectory(f); + } + + @Test + public void testPrintJsonValidOutput() { + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + KeystoreCliUtils.printJson(new PrintWriter(out), new PrintWriter(err), + KeystoreCliUtils.jsonMap("address", "TAddr", "file", "file.json")); + String s = out.toString().trim(); + assertTrue(s.contains("\"address\":\"TAddr\"")); + assertTrue(s.contains("\"file\":\"file.json\"")); + } + + @Test + public void testPrintSecurityTipsIncludesAddressAndFile() { + StringWriter out = new StringWriter(); + KeystoreCliUtils.printSecurityTips(new PrintWriter(out), + "TMyAddress", "/path/to/keystore.json"); + String s = out.toString(); + assertTrue(s.contains("TMyAddress")); + assertTrue(s.contains("/path/to/keystore.json")); + assertTrue(s.contains("NEVER share")); + assertTrue(s.contains("BACKUP")); + assertTrue(s.contains("REMEMBER")); + } + + @Test + public void testReadRegularFileSuccess() throws Exception { + File f = tempFolder.newFile("regular.txt"); + Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", + new PrintWriter(err)); + assertNotNull(bytes); + assertEquals("hello", new String(bytes, StandardCharsets.UTF_8)); + } + + @Test + public void testReadRegularFileMissing() throws Exception { + File f = new File(tempFolder.getRoot(), "does-not-exist"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "Password file", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected 'not found' error, got: " + err.toString(), + err.toString().contains("Password file not found")); + } + + @Test + public void testReadRegularFileTooLarge() throws Exception { + File f = tempFolder.newFile("big.txt"); + byte[] big = new byte[2048]; + java.util.Arrays.fill(big, (byte) 'a'); + Files.write(f.toPath(), big); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "Password file", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected 'too large', got: " + err.toString(), + err.toString().contains("too large")); + } + + @Test + public void testReadRegularFileRefusesSymlink() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File target = tempFolder.newFile("real-target.txt"); + Files.write(target.toPath(), "secret content".getBytes(StandardCharsets.UTF_8)); + File link = new File(tempFolder.getRoot(), "symlink.txt"); + Files.createSymbolicLink(link.toPath(), target.toPath()); + + StringWriter err = new StringWriter(); + byte[] bytes = KeystoreCliUtils.readRegularFile(link, 1024, "File", + new PrintWriter(err)); + + assertNull("Must refuse to read through symlink", bytes); + assertTrue("Expected symlink-refusal message, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testReadRegularFileRefusesDirectory() throws Exception { + File dir = tempFolder.newFolder("a-dir"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(dir, 1024, "File", + new PrintWriter(err)); + assertNull(bytes); + assertTrue("Expected not-regular-file error, got: " + err.toString(), + err.toString().contains("Not a regular file")); + } + + @Test + public void testReadRegularFileEmptyFile() throws Exception { + File f = tempFolder.newFile("empty.txt"); + StringWriter err = new StringWriter(); + + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", + new PrintWriter(err)); + assertNotNull(bytes); + assertEquals(0, bytes.length); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java new file mode 100644 index 00000000000..3e718dfd143 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java @@ -0,0 +1,536 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.common.utils.ByteArray; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreImportTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testImportWithKeyFileAndPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore"); + + // Generate a known private key + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String expectedAddress = Credentials.create(keyPair).getAddress(); + + File keyFile = tempFolder.newFile("private.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("password.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify roundtrip: decrypt should recover the same private key + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertEquals("Address must match", expectedAddress, creds.getAddress()); + assertArrayEquals("Private key must survive import roundtrip", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportInvalidKeyTooShort() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + File keyFile = tempFolder.newFile("bad.key"); + Files.write(keyFile.toPath(), "abcdef1234".getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with invalid key", 1, exitCode); + } + + @Test + public void testImportInvalidKeyNonHex() throws Exception { + File dir = tempFolder.newFolder("keystore-hex"); + File keyFile = tempFolder.newFile("nonhex.key"); + Files.write(keyFile.toPath(), + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + .getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with non-hex key", 1, exitCode); + } + + @Test + public void testImportNoTtyNoKeyFile() throws Exception { + File dir = tempFolder.newFolder("keystore-notty"); + File pwFile = tempFolder.newFile("pw2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // No --key-file and System.console() is null in CI + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --key-file", 1, exitCode); + } + + @Test + public void testImportWithSm2() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + // SM2 uses same 32-byte private key format + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), false); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("sm2.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 import should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify SM2 keystore can be decrypted + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], false); + assertArrayEquals("SM2 key must survive import roundtrip", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportKeyFileWithWhitespace() throws Exception { + File dir = tempFolder.newFolder("keystore-ws"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + // Key file with leading/trailing whitespace and newlines + File keyFile = tempFolder.newFile("ws.key"); + Files.write(keyFile.toPath(), + (" " + privateKeyHex + " \n\n").getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-ws.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with whitespace-padded key should succeed", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertArrayEquals("Key must survive whitespace-trimmed import", + keyPair.getPrivateKey(), creds.getSignInterface().getPrivateKey()); + } + + @Test + public void testImportDuplicateAddressBlocked() throws Exception { + File dir = tempFolder.newFolder("keystore-dup"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("dup.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-dup.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // First import succeeds + CommandLine cmd1 = new CommandLine(new Toolkit()); + assertEquals(0, cmd1.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + + // Second import of same key is blocked + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd2 = new CommandLine(new Toolkit()); + cmd2.setErr(new java.io.PrintWriter(err)); + assertEquals("Duplicate import should be blocked", 1, + cmd2.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + assertTrue("Error should mention already exists", + err.toString().contains("already exists")); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Should still have only 1 keystore", 1, files.length); + } + + @Test + public void testImportDuplicateAddressWithForce() throws Exception { + File dir = tempFolder.newFolder("keystore-force"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("force.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-force.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + // First import + CommandLine cmd1 = new CommandLine(new Toolkit()); + assertEquals(0, cmd1.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath())); + + // Second import with --force succeeds + CommandLine cmd2 = new CommandLine(new Toolkit()); + assertEquals(0, cmd2.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--force")); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Force import should create 2 files", 2, files.length); + } + + @Test + public void testImportKeyFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nokey"); + File pwFile = tempFolder.newFile("pw-nokey.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", "/tmp/nonexistent-key-file.txt", + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when key file not found", 1, exitCode); + } + + @Test + public void testImportWith0xPrefix() throws Exception { + File dir = tempFolder.newFolder("keystore-0x"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String expectedAddress = Credentials.create(keyPair).getAddress(); + + File keyFile = tempFolder.newFile("0x.key"); + Files.write(keyFile.toPath(), + ("0x" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-0x.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with 0x prefix should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertEquals("Address must match", expectedAddress, creds.getAddress()); + } + + @Test + public void testImportWith0XUppercasePrefix() throws Exception { + File dir = tempFolder.newFolder("keystore-0X"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("0X.key"); + Files.write(keyFile.toPath(), + ("0X" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-0X.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import with 0X prefix should succeed", 0, exitCode); + } + + @Test + public void testImportWarnsOnCorruptedFile() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + // Create a corrupted JSON in the keystore dir + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + File keyFile = tempFolder.newFile("warn.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-warn.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter out = new java.io.StringWriter(); + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new java.io.PrintWriter(out)); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + String errOutput = err.toString(); + assertTrue("Should warn about corrupted file", + errOutput.contains("Warning: skipping unreadable file: corrupted.json")); + } + + @Test + public void testImportKeystoreFilePermissions() throws Exception { + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("perm.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-perm.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + java.util.Set perms = + Files.getPosixFilePermissions(files[0].toPath()); + assertEquals("Keystore file should have owner-only permissions (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testImportRefusesSymlinkKeyFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-symlink"); + // Create a real key file and a symlink pointing to it + File target = tempFolder.newFile("real.key"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + Files.write(target.toPath(), + ByteArray.toHexString(keyPair.getPrivateKey()).getBytes(StandardCharsets.UTF_8)); + + File symlink = new File(tempFolder.getRoot(), "symlink.key"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File pwFile = tempFolder.newFile("pw-symlink.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", symlink.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Must refuse symlinked key file", 1, exitCode); + assertTrue("Expected symlink-refusal error, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testImportRefusesSymlinkPasswordFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-pwsymlink"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + File keyFile = tempFolder.newFile("sym-pw.key"); + Files.write(keyFile.toPath(), + ByteArray.toHexString(keyPair.getPrivateKey()).getBytes(StandardCharsets.UTF_8)); + + File realPwFile = tempFolder.newFile("real-pw.txt"); + Files.write(realPwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + File pwSymlink = new File(tempFolder.getRoot(), "pw-symlink.txt"); + Files.createSymbolicLink(pwSymlink.toPath(), realPwFile.toPath()); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwSymlink.getAbsolutePath()); + + assertEquals("Must refuse symlinked password file", 1, exitCode); + assertTrue("Expected symlink-refusal error, got: " + err.toString(), + err.toString().contains("Refusing to follow symbolic link")); + } + + @Test + public void testImportDuplicateCheckSkipsInvalidVersion() throws Exception { + File dir = tempFolder.newFolder("keystore-badver"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + String address = Credentials.create(keyPair).getAddress(); + + // Create a JSON with correct address but wrong version — should NOT count as duplicate + String fakeKeystore = "{\"address\":\"" + address + + "\",\"version\":2,\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "fake.json").toPath(), + fakeKeystore.getBytes(StandardCharsets.UTF_8)); + + File keyFile = tempFolder.newFile("ver.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-ver.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import should succeed — invalid-version file is not a real duplicate", 0, + exitCode); + } + + @Test + public void testImportDuplicateScanSkipsSymlinkedEntry() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-dup-symlink"); + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File keyFile = tempFolder.newFile("dup-sym.key"); + Files.write(keyFile.toPath(), + privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("pw-dup-sym.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Import should succeed with symlink present", 0, exitCode); + assertTrue("Duplicate scan must warn about the symlinked entry, got: " + + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + } + + @Test + public void testImportRejectsMultiLinePasswordFile() throws Exception { + // Regression: a user might accidentally point --password-file at a + // `keystore update` two-line file; without the guard that literal + // "old\nnew" becomes the password. + File dir = tempFolder.newFolder("keystore-multi-pw"); + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String privateKeyHex = ByteArray.toHexString(keyPair.getPrivateKey()); + + File keyFile = tempFolder.newFile("multi.key"); + Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + File pwFile = tempFolder.newFile("multi-pw.txt"); + Files.write(pwFile.toPath(), + "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + + java.io.StringWriter err = new java.io.StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new java.io.PrintWriter(err)); + int exitCode = cmd.execute("keystore", "import", + "--keystore-dir", dir.getAbsolutePath(), + "--key-file", keyFile.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should reject multi-line password file", 1, exitCode); + assertTrue("Error must explain the multi-line rejection, got: " + err.toString(), + err.toString().contains("multiple lines")); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertTrue("No keystore should have been created", + files == null || files.length == 0); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java new file mode 100644 index 00000000000..b029ddaf9f7 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreListTest.java @@ -0,0 +1,282 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreListTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testListMultipleKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore"); + String password = "test123456"; + + // Create 3 keystores + for (int i = 0; i < 3; i++) { + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + } + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Output should not be empty", output.length() > 0); + // Should have 3 lines of output (one per keystore) + String[] lines = output.split("\\n"); + assertEquals("Should list 3 keystores", 3, lines.length); + // Each line should contain a T-address and a .json filename + for (String line : lines) { + assertTrue("Each line should contain an address starting with T", + line.trim().startsWith("T")); + assertTrue("Each line should reference a .json file", + line.contains(".json")); + } + } + + @Test + public void testListEmptyDirectory() throws Exception { + File dir = tempFolder.newFolder("empty"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should print no-keystores message", + out.toString().contains("No keystores found")); + } + + @Test + public void testListNonExistentDirectory() throws Exception { + File dir = new File(tempFolder.getRoot(), "nonexistent"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should print no-keystores message", + out.toString().contains("No keystores found")); + } + + @Test + public void testListEmptyDirectoryJsonOutput() throws Exception { + File dir = tempFolder.newFolder("empty-json"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Empty dir JSON should have empty keystores array", + output.contains("{\"keystores\":[]}")); + } + + @Test + public void testListNonExistentDirectoryJsonOutput() throws Exception { + File dir = new File(tempFolder.getRoot(), "nonexistent-json"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Non-existent dir JSON should have empty keystores array", + output.contains("{\"keystores\":[]}")); + } + + @Test + public void testListJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + String password = "test123456"; + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath(), "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Should start with keystores JSON array", + output.startsWith("{\"keystores\":[")); + assertTrue("Should end with JSON array close", + output.endsWith("]}")); + } + + @Test + public void testListSkipsNonKeystoreFiles() throws Exception { + File dir = tempFolder.newFolder("keystore-mixed"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create non-keystore files + Files.write(new File(dir, "readme.json").toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + Files.write(new File(dir, "notes.txt").toPath(), + "plain text".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("Output should not be empty", output.length() > 0); + String[] lines = output.split("\\n"); + // Should list only the valid keystore, not the readme.json or notes.txt + assertEquals("Should list only 1 valid keystore", 1, lines.length); + } + + @Test + public void testListWarnsOnCorruptedJsonFiles() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create a corrupted JSON file + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String errOutput = err.toString(); + assertTrue("Should warn about corrupted file", + errOutput.contains("Warning: skipping unreadable file: corrupted.json")); + + // Valid keystore should still be listed + String output = out.toString().trim(); + assertTrue("Should still list the valid keystore", output.length() > 0); + } + + @Test + public void testListSkipsInvalidVersionKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore-version"); + String password = "test123456"; + + // Create one valid keystore + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // Create a JSON with address and crypto but wrong version + String fakeV2 = "{\"address\":\"TFakeAddress\",\"version\":2," + + "\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "v2-keystore.json").toPath(), + fakeV2.getBytes(StandardCharsets.UTF_8)); + + // Create a JSON with address but null crypto + String noCrypto = "{\"address\":\"TFakeAddress2\",\"version\":3}"; + Files.write(new File(dir, "no-crypto.json").toPath(), + noCrypto.getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + String[] lines = output.split("\\n"); + assertEquals("Should list only the valid v3 keystore, not v2 or no-crypto", + 1, lines.length); + } + + @Test + public void testListSkipsSymlinkedKeystoreFile() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-symlink-scan"); + String password = "test123456"; + + SignInterface key = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, key, dir, false); + + // A JSON file elsewhere (simulates "target we should not be tricked + // into reading") — placed outside the keystore dir. + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"secret\":\"should not appear in list output\"}" + .getBytes(StandardCharsets.UTF_8)); + + // Plant a .json symlink in the keystore dir + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "list", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should warn about symbolic link, got err: " + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + String output = out.toString().trim(); + String[] lines = output.split("\\n"); + assertEquals("Should list only the real keystore", 1, lines.length); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java new file mode 100644 index 00000000000..0819103642e --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java @@ -0,0 +1,307 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreNewTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testNewKeystoreWithPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore"); + File pwFile = tempFolder.newFile("password.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals("Should create exactly one keystore file", 1, files.length); + + // Verify the file is a valid keystore + Credentials creds = WalletUtils.loadCredentials("test123456", files[0], true); + assertNotNull(creds.getAddress()); + assertTrue(creds.getAddress().startsWith("T")); + } + + @Test + public void testNewKeystoreJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + File pwFile = tempFolder.newFile("password-json.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("JSON output should contain address", + output.contains("\"address\"")); + assertTrue("JSON output should contain file", + output.contains("\"file\"")); + } + + @Test + public void testNewKeystoreInvalidPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + File pwFile = tempFolder.newFile("short.txt"); + Files.write(pwFile.toPath(), "abc".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with short password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testNewKeystoreCustomDir() throws Exception { + File dir = new File(tempFolder.getRoot(), "custom/nested/dir"); + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Custom dir should be created", dir.exists()); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + } + + @Test + public void testNewKeystoreNoTtyNoPasswordFile() throws Exception { + // In CI/test environment, System.console() is null. + // Without --password-file, should fail with exit code 1. + File dir = tempFolder.newFolder("keystore-notty"); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --password-file", 1, exitCode); + } + + @Test + public void testNewKeystoreEmptyPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-empty"); + File pwFile = tempFolder.newFile("empty.txt"); + Files.write(pwFile.toPath(), "".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with empty password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testNewKeystoreWithSm2() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 keystore creation should succeed", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify SM2 keystore can be decrypted with ecKey=false + org.tron.keystore.Credentials creds = + org.tron.keystore.WalletUtils.loadCredentials("test123456", files[0], false); + assertNotNull(creds.getAddress()); + } + + @Test + public void testNewKeystoreSpecialCharPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-special"); + File pwFile = tempFolder.newFile("pw-special.txt"); + String password = "p@$$w0rd!#%^&*()_+-=[]{}"; + Files.write(pwFile.toPath(), password.getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + // Verify can decrypt with same special-char password + Credentials creds = WalletUtils.loadCredentials(password, files[0], true); + assertNotNull(creds.getAddress()); + } + + @Test + public void testNewKeystorePasswordFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nopw"); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", "/tmp/nonexistent-pw.txt"); + + assertEquals("Should fail when password file not found", 1, exitCode); + } + + @Test + public void testNewKeystoreDirIsFile() throws Exception { + File notADir = tempFolder.newFile("not-a-dir"); + File pwFile = tempFolder.newFile("pw-dir.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", notADir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail when dir is a file", 1, exitCode); + } + + @Test + public void testNewKeystorePasswordFileTooLarge() throws Exception { + File dir = tempFolder.newFolder("keystore-bigpw"); + File pwFile = tempFolder.newFile("bigpw.txt"); + byte[] bigContent = new byte[1025]; + java.util.Arrays.fill(bigContent, (byte) 'a'); + Files.write(pwFile.toPath(), bigContent); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with large password file", 1, exitCode); + assertTrue("Error should mention file too large", + err.toString().contains("too large")); + } + + @Test + public void testNewKeystorePasswordFileWithBom() throws Exception { + File dir = tempFolder.newFolder("keystore-bom"); + File pwFile = tempFolder.newFile("bom.txt"); + Files.write(pwFile.toPath(), + ("\uFEFF" + "test123456").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should succeed with BOM password file", 0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + } + + @Test + public void testNewKeystoreFilePermissions() throws Exception { + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + File pwFile = tempFolder.newFile("pw-perms.txt"); + Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertNotNull(files); + assertEquals(1, files.length); + + java.util.Set perms = + Files.getPosixFilePermissions(files[0].toPath()); + assertEquals("Keystore file should have owner-only permissions (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testNewKeystoreRejectsMultiLinePasswordFile() throws Exception { + // Regression: a user might accidentally point --password-file at a + // `keystore update` two-line file (old\nnew). Without the guard the + // literal "old\nnew" becomes the password and neither line alone can + // unlock it later. new/import must reject multi-line files. + File dir = tempFolder.newFolder("keystore-multi"); + File pwFile = tempFolder.newFile("multi-line.txt"); + Files.write(pwFile.toPath(), + "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "new", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should reject multi-line password file", 1, exitCode); + assertTrue("Error must explain the multi-line rejection, got: " + err.toString(), + err.toString().contains("multiple lines")); + // No keystore created + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + assertTrue("No keystore should have been created", + files == null || files.length == 0); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java new file mode 100644 index 00000000000..cdbe9b3816b --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java @@ -0,0 +1,822 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.SecureRandom; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.SignUtils; +import org.tron.keystore.Credentials; +import org.tron.keystore.WalletFile; +import org.tron.keystore.WalletUtils; +import picocli.CommandLine; + +public class KeystoreUpdateTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void testUpdatePassword() throws Exception { + File dir = tempFolder.newFolder("keystore"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + String address = creds.getAddress(); + + File pwFile = tempFolder.newFile("passwords.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Exit code should be 0", 0, exitCode); + + // Verify: new password works and key survives + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive password change", + originalKey, updated.getSignInterface().getPrivateKey()); + + // Verify: address field preserved in keystore JSON + WalletFile wf = MAPPER.readValue(new File(dir, fileName), WalletFile.class); + assertEquals("Address must be preserved in updated keystore", + address, wf.getAddress()); + } + + @Test + public void testUpdateWrongOldPassword() throws Exception { + File dir = tempFolder.newFolder("keystore-bad"); + String password = "correct123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + String address = creds.getAddress(); + + File pwFile = tempFolder.newFile("wrong.txt"); + Files.write(pwFile.toPath(), + ("wrongpass1\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with wrong password", 1, exitCode); + assertTrue("Error should mention decryption", + err.toString().contains("Decryption failed")); + + // Verify: original password still works (file unchanged) + Credentials unchanged = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + assertEquals(address, unchanged.getAddress()); + } + + @Test + public void testUpdateNonExistentAddress() throws Exception { + File dir = tempFolder.newFolder("keystore-noaddr"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + WalletUtils.generateWalletFile(password, keyPair, dir, true); + + File pwFile = tempFolder.newFile("pw.txt"); + Files.write(pwFile.toPath(), + ("test123456\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TNonExistentAddress123456789", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail for non-existent address", 1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateNewPasswordTooShort() throws Exception { + File dir = tempFolder.newFolder("keystore-shortpw"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("shortpw.txt"); + Files.write(pwFile.toPath(), + (password + "\nabc").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with short new password", 1, exitCode); + assertTrue("Error should mention password length", + err.toString().contains("at least 6 characters")); + } + + @Test + public void testUpdateWithWindowsLineEndings() throws Exception { + File dir = tempFolder.newFolder("keystore-crlf"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("crlf.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\r\n" + newPassword + "\r\n").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with CRLF password file should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with CRLF passwords", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateJsonOutput() throws Exception { + File dir = tempFolder.newFolder("keystore-json"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("pw-json.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--json"); + + assertEquals(0, exitCode); + String output = out.toString().trim(); + assertTrue("JSON should contain address", + output.contains("\"address\"")); + assertTrue("JSON should contain status updated", + output.contains("\"updated\"")); + assertTrue("JSON should contain file", + output.contains("\"file\"")); + } + + @Test + public void testUpdateWarnsOnCorruptedFile() throws Exception { + File dir = tempFolder.newFolder("keystore-corrupt"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + Files.write(new File(dir, "corrupted.json").toPath(), + "not valid json{{{".getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw-corrupt.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + assertTrue("Should warn about corrupted file", + err.toString().contains("Warning: skipping unreadable file: corrupted.json")); + } + + @Test + public void testUpdatePasswordFileOnlyOneLine() throws Exception { + File dir = tempFolder.newFolder("keystore-1line"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + File pwFile = tempFolder.newFile("oneline.txt"); + Files.write(pwFile.toPath(), + "onlyoldpassword".getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with single-line password file", 1, exitCode); + assertTrue("Error should mention exactly two lines", + err.toString().contains("exactly two lines")); + } + + @Test + public void testUpdatePasswordFileThreeLines() throws Exception { + // Regression: a three-line password file (e.g. someone confusingly added a + // confirm line, or pointed at the wrong file) must be rejected, not have + // the third line silently discarded. The original keystore must remain + // decryptable with the old password. + File dir = tempFolder.newFolder("keystore-3line"); + String oldPassword = "oldpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Snapshot the keystore bytes so we can verify the file is untouched. + byte[] beforeBytes = Files.readAllBytes(new File(dir, fileName).toPath()); + + File pwFile = tempFolder.newFile("threeline.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\nnewpass456\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with three-line password file", 1, exitCode); + assertTrue("Error should mention exactly two lines, got: " + err.toString(), + err.toString().contains("exactly two lines")); + + // Verify: keystore file is byte-for-byte unchanged + byte[] afterBytes = Files.readAllBytes(new File(dir, fileName).toPath()); + assertArrayEquals("Keystore file must not be modified on rejection", + beforeBytes, afterBytes); + + // Verify: original password still decrypts the keystore + Credentials unchanged = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + assertArrayEquals("Original key must still be recoverable with old password", + originalKey, unchanged.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateNoTtyNoPasswordFile() throws Exception { + File dir = tempFolder.newFolder("keystore-notty"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath()); + + assertEquals("Should fail when no TTY and no --password-file", 1, exitCode); + assertTrue("Error should mention no terminal", + err.toString().contains("No interactive terminal")); + } + + @Test + public void testUpdatePasswordFileNotFound() throws Exception { + File dir = tempFolder.newFolder("keystore-nopwf"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", "/tmp/nonexistent-pw-update.txt"); + + assertEquals("Should fail when password file not found", 1, exitCode); + assertTrue("Error should mention file not found", + err.toString().contains("Password file not found")); + } + + @Test + public void testUpdateSm2Keystore() throws Exception { + File dir = tempFolder.newFolder("keystore-sm2"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), false); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), false); + + File pwFile = tempFolder.newFile("pw-sm2.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath(), + "--sm2"); + + assertEquals("SM2 keystore update should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), false); + assertArrayEquals("SM2 key must survive password change", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateMultipleKeystoresSameAddress() throws Exception { + File dir = tempFolder.newFolder("keystore-multi"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String address = Credentials.create(keyPair).getAddress(); + + // Create two keystores for the same address via direct API + WalletUtils.generateWalletFile(password, keyPair, dir, true); + // Small delay to get different filename timestamps + Thread.sleep(50); + WalletUtils.generateWalletFile(password, keyPair, dir, true); + + File pwFile = tempFolder.newFile("pw-multi.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with multiple keystores for same address", 1, exitCode); + assertTrue("Error should mention multiple keystores", + err.toString().contains("Multiple keystores found")); + assertTrue("Error should mention remove duplicates", + err.toString().contains("remove duplicates")); + } + + @Test + public void testUpdatePasswordFileTooLarge() throws Exception { + File dir = tempFolder.newFolder("keystore-bigpw"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(password, + new File(dir, fileName), true); + + // Create a password file > 1KB + File pwFile = tempFolder.newFile("bigpw.txt"); + byte[] bigContent = new byte[1025]; + java.util.Arrays.fill(bigContent, (byte) 'a'); + Files.write(pwFile.toPath(), bigContent); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail with large password file", 1, exitCode); + assertTrue("Error should mention file too large", + err.toString().contains("too large")); + } + + @Test + public void testUpdatePasswordFileWithBom() throws Exception { + File dir = tempFolder.newFolder("keystore-bom"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Password file with UTF-8 BOM + File pwFile = tempFolder.newFile("bom.txt"); + Files.write(pwFile.toPath(), + ("\uFEFF" + oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with BOM password file should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with BOM password file", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateNonExistentKeystoreDir() throws Exception { + File dir = new File(tempFolder.getRoot(), "does-not-exist"); + + File pwFile = tempFolder.newFile("pw-nodir.txt"); + Files.write(pwFile.toPath(), + ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TSomeAddress", + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateKeystoreDirIsFile() throws Exception { + File notADir = tempFolder.newFile("not-a-dir"); + + File pwFile = tempFolder.newFile("pw-notdir.txt"); + Files.write(pwFile.toPath(), + ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", "TSomeAddress", + "--keystore-dir", notADir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found for address")); + } + + @Test + public void testUpdateWithOldMacLineEndings() throws Exception { + File dir = tempFolder.newFolder("keystore-cr"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + byte[] originalKey = keyPair.getPrivateKey(); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Password file with old Mac line endings (\r only) + File pwFile = tempFolder.newFile("cr.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\r" + newPassword + "\r").getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update with old Mac CR line endings should succeed", 0, exitCode); + + Credentials updated = WalletUtils.loadCredentials(newPassword, + new File(dir, fileName), true); + assertArrayEquals("Key must survive update with CR passwords", + originalKey, updated.getSignInterface().getPrivateKey()); + } + + @Test + public void testUpdateSkipsInvalidVersionKeystores() throws Exception { + File dir = tempFolder.newFolder("keystore-badver"); + String password = "test123456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String address = Credentials.create(keyPair).getAddress(); + + // Create a JSON file with correct address but wrong version + String fakeKeystore = "{\"address\":\"" + address + + "\",\"version\":2,\"crypto\":{\"cipher\":\"aes-128-ctr\"}}"; + Files.write(new File(dir, "fake.json").toPath(), + fakeKeystore.getBytes(StandardCharsets.UTF_8)); + + File pwFile = tempFolder.newFile("pw-badver.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", address, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should not find keystore with wrong version", 1, exitCode); + assertTrue("Error should mention no keystore found", + err.toString().contains("No keystore found")); + } + + @Test + public void testUpdateRejectsTamperedAddressKeystore() throws Exception { + File dir = tempFolder.newFolder("keystore-tampered"); + String password = "test123456"; + + // Create a real keystore, then tamper with the address field to simulate + // a spoofed keystore that claims a different address than its encrypted key. + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(password, keyPair, dir, true); + File keystoreFile = new File(dir, fileName); + + String realAddress = Credentials.create(keyPair).getAddress(); + String spoofedAddress = "TSpoofedAddressXXXXXXXXXXXXXXXXXXXX"; + + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper() + .configure(com.fasterxml.jackson.databind.DeserializationFeature + .FAIL_ON_UNKNOWN_PROPERTIES, false); + org.tron.keystore.WalletFile wf = mapper.readValue(keystoreFile, + org.tron.keystore.WalletFile.class); + wf.setAddress(spoofedAddress); + mapper.writeValue(keystoreFile, wf); + + File pwFile = tempFolder.newFile("pw-tampered.txt"); + Files.write(pwFile.toPath(), + (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", spoofedAddress, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Should fail decryption on tampered address", 1, exitCode); + assertTrue("Error should mention address mismatch, got: " + err.toString(), + err.toString().contains("address mismatch")); + } + + @Test + public void testUpdatePreservesCorrectDerivedAddress() throws Exception { + // After update, the keystore's address field should be the derived address, + // not carried over from the original JSON (defense-in-depth against any + // residual spoofed address that somehow passed decryption). + File dir = tempFolder.newFolder("keystore-derived"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + String originalAddress = Credentials.create(keyPair).getAddress(); + + File pwFile = tempFolder.newFile("pw-derived.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", originalAddress, + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + + // Verify updated file has the derived address + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + org.tron.keystore.WalletFile wf = mapper.readValue(new File(dir, fileName), + org.tron.keystore.WalletFile.class); + assertEquals("Updated keystore address must match derived address", + originalAddress, wf.getAddress()); + } + + @Test + public void testUpdateNarrowsLoosePermissionsTo0600() throws Exception { + // Adversarial test: pre-loosen the keystore to 0644, then verify that + // update writes the file back with 0600. This exercises the temp-file + // + atomic-rename path rather than merely preserving existing perms. + String os = System.getProperty("os.name").toLowerCase(); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + !os.contains("win")); + + File dir = tempFolder.newFolder("keystore-perms"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + // Deliberately loosen to 0644 before update + java.nio.file.Path keystorePath = new File(dir, fileName).toPath(); + java.nio.file.Files.setPosixFilePermissions(keystorePath, + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE, + java.nio.file.attribute.PosixFilePermission.GROUP_READ, + java.nio.file.attribute.PosixFilePermission.OTHERS_READ)); + + File pwFile = tempFolder.newFile("pw-perms.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + CommandLine cmd = new CommandLine(new Toolkit()); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(0, exitCode); + + // Verify the updated keystore file is now owner-only (0600), not 0644 + java.util.Set perms = + java.nio.file.Files.getPosixFilePermissions(keystorePath); + assertEquals("Updated keystore must be narrowed to owner-only (rw-------)", + java.util.EnumSet.of( + java.nio.file.attribute.PosixFilePermission.OWNER_READ, + java.nio.file.attribute.PosixFilePermission.OWNER_WRITE), + perms); + } + + @Test + public void testUpdateLegacyTipFiresWhenPasswordHasWhitespace() throws Exception { + // The legacy-truncation tip should fire when the entered old password + // contains whitespace and decryption fails — the scenario that actually + // matches the legacy bug. + File dir = tempFolder.newFolder("keystore-tip-ws"); + String realPassword = "realpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(realPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(realPassword, + new File(dir, fileName), true); + + // Password with internal whitespace that is NOT the real password + File pwFile = tempFolder.newFile("pw-ws.txt"); + Files.write(pwFile.toPath(), + ("correct horse battery staple\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Legacy-truncation tip should fire for whitespace password, got: " + + err.toString(), + err.toString().contains("first whitespace-separated word")); + } + + @Test + public void testUpdateLegacyTipSuppressedWhenPasswordHasNoWhitespace() throws Exception { + // For the common "wrong password" case (no whitespace), the legacy tip + // would be noise — it should be suppressed. + File dir = tempFolder.newFolder("keystore-tip-nows"); + String realPassword = "realpass123"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(realPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(realPassword, + new File(dir, fileName), true); + + // Wrong password with no whitespace + File pwFile = tempFolder.newFile("pw-nows.txt"); + Files.write(pwFile.toPath(), + ("wrongpassword\nnewpass789").getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals(1, exitCode); + assertTrue("Decryption failure must still be reported", + err.toString().contains("Decryption failed")); + assertFalse("Legacy-truncation tip should NOT fire for whitespace-free password", + err.toString().contains("first whitespace-separated word")); + } + + @Test + public void testUpdateScanSkipsSymlinkedEntry() throws Exception { + org.junit.Assume.assumeTrue("Symlinks only tested on POSIX", + !System.getProperty("os.name").toLowerCase().contains("win")); + + File dir = tempFolder.newFolder("keystore-update-symlink"); + String oldPassword = "oldpass123"; + String newPassword = "newpass456"; + + SignInterface keyPair = SignUtils.getGeneratedRandomSign( + SecureRandom.getInstance("NativePRNG"), true); + String fileName = WalletUtils.generateWalletFile(oldPassword, keyPair, dir, true); + Credentials creds = WalletUtils.loadCredentials(oldPassword, + new File(dir, fileName), true); + + File target = tempFolder.newFile("outside.json"); + Files.write(target.toPath(), + "{\"not\":\"a keystore\"}".getBytes(StandardCharsets.UTF_8)); + File symlink = new File(dir, "evil.json"); + Files.createSymbolicLink(symlink.toPath(), target.toPath()); + + File pwFile = tempFolder.newFile("pw-update-sym.txt"); + Files.write(pwFile.toPath(), + (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + + StringWriter err = new StringWriter(); + CommandLine cmd = new CommandLine(new Toolkit()); + cmd.setErr(new PrintWriter(err)); + int exitCode = cmd.execute("keystore", "update", creds.getAddress(), + "--keystore-dir", dir.getAbsolutePath(), + "--password-file", pwFile.getAbsolutePath()); + + assertEquals("Update should succeed; symlinked entry must not break scan", 0, exitCode); + assertTrue("Scan must warn about the symlinked entry, got: " + err.toString(), + err.toString().contains("Warning: skipping symbolic link: evil.json")); + } +} diff --git a/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java b/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java index f5880d82e39..ff73eca25cc 100644 --- a/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java +++ b/plugins/src/test/java/org/tron/plugins/leveldb/ArchiveManifestTest.java @@ -1,16 +1,7 @@ package org.tron.plugins.leveldb; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Properties; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -82,41 +73,4 @@ public void testEmpty() { Assert.assertEquals(0, ArchiveManifest.run(args)); } - private static void writeProperty(String filename, String key, String value) throws IOException { - File file = new File(filename); - if (!file.exists()) { - file.createNewFile(); - } - - try (FileInputStream fis = new FileInputStream(file); - OutputStream out = new FileOutputStream(file); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, - StandardCharsets.UTF_8))) { - BufferedReader bf = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); - Properties properties = new Properties(); - properties.load(bf); - properties.setProperty(key, value); - properties.store(bw, "Generated by the application. PLEASE DO NOT EDIT! "); - } catch (Exception e) { - logger.warn("{}", e); - } - } - - /** - * delete directory. - */ - private static boolean deleteDir(File dir) { - if (dir.isDirectory()) { - String[] children = dir.list(); - assert children != null; - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - logger.warn("can't delete dir:" + dir); - return false; - } - } - } - return dir.delete(); - } } diff --git a/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java b/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java index 69dca01e4f8..8c20c2b55be 100644 --- a/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java +++ b/plugins/src/test/java/org/tron/plugins/leveldb/DbArchiveTest.java @@ -1,16 +1,7 @@ package org.tron.plugins.leveldb; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.Properties; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -88,41 +79,4 @@ public void testEmpty() { Assert.assertEquals(0, cli.execute(args)); } - private static void writeProperty(String filename, String key, String value) throws IOException { - File file = new File(filename); - if (!file.exists()) { - file.createNewFile(); - } - - try (FileInputStream fis = new FileInputStream(file); - OutputStream out = new FileOutputStream(file); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, - StandardCharsets.UTF_8))) { - BufferedReader bf = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); - Properties properties = new Properties(); - properties.load(bf); - properties.setProperty(key, value); - properties.store(bw, "Generated by the application. PLEASE DO NOT EDIT! "); - } catch (Exception e) { - logger.warn("{}", e); - } - } - - /** - * delete directory. - */ - private static boolean deleteDir(File dir) { - if (dir.isDirectory()) { - String[] children = dir.list(); - assert children != null; - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - logger.warn("can't delete dir:" + dir); - return false; - } - } - } - return dir.delete(); - } } diff --git a/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java index ab1067fefc3..ebc4074ccc0 100644 --- a/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java +++ b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteRocksDbV2Test.java @@ -7,7 +7,7 @@ public class DbLiteRocksDbV2Test extends DbLiteTest { @Test - public void testToolsWithRocksDB() throws InterruptedException, IOException { + public void testToolsWithRocksDbV2() throws InterruptedException, IOException { testTools("ROCKSDB", 2); } } diff --git a/quickstart.md b/quickstart.md index 6eda855f1e9..b3eeb7b7713 100644 --- a/quickstart.md +++ b/quickstart.md @@ -45,7 +45,7 @@ docker pull tronprotocol/java-tron You can run the command below to start the java-tron: ``` -docker run -it -d -p 8090:8090 -p 8091:8091 -p 18888:18888 -p 50051:50051 --restart always tronprotocol/java-tron +docker run -it -d -p 8090:8090 -p 18888:18888 -p 50051:50051 --restart always tronprotocol/java-tron ``` The `-p` flag defines the ports that the container needs to be mapped on the host machine. By default the container will start and join in the mainnet @@ -65,8 +65,9 @@ Note: The directory `/Users/tron/docker/conf` must contain the file `config-loca ## Quickstart for using docker-tron-quickstart -The image exposes a Full Node, Solidity Node, and Event Server. Through TRON Quickstart, users can deploy DApps, smart contracts, and interact with the TronWeb library. -Check more information at [Quickstart:](https://github.com/TRON-US/docker-tron-quickstart) +The image exposes a Full Node and Event Server. Through TRON Quickstart, users can deploy DApps, smart contracts, and interact with the TronWeb library. + +> Note: `docker-tron-quickstart` is a community-maintained tool. Check its repository for the latest status: [Quickstart](https://github.com/TRON-US/docker-tron-quickstart) ### Node.JS Console Node.JS is used to interact with the Full and Solidity Nodes via Tron-Web. @@ -84,7 +85,7 @@ docker pull trontools/quickstart ## Setup TRON Quickstart ### TRON Quickstart Run -Run the "docker run" command to launch TRON Quickstart. TRON Quickstart exposes port 9090 for Full Node, Solidity Node, and Event Server. +Run the "docker run" command to launch TRON Quickstart. TRON Quickstart exposes port 9090 for Full Node and Event Server. ```shell docker run -it \ -p 9090:9090 \ diff --git a/run.md b/run.md deleted file mode 100644 index c0ecbe4d91f..00000000000 --- a/run.md +++ /dev/null @@ -1,193 +0,0 @@ -# How to Running - -### Running multi-nodes - -https://github.com/tronprotocol/Documentation/blob/master/TRX/Solidity_and_Full_Node_Deployment_EN.md - -## Running a local node and connecting to the public testnet - -Use the [Testnet Config](https://github.com/tronprotocol/TronDeployment/blob/master/test_net_config.conf) or use the [Tron Deployment Scripts](https://github.com/tronprotocol/TronDeployment). - - -### Running a Super Representative Node for mainnet - -**Use the executable JAR(Recommended way):** - -```bash -java -jar FullNode.jar -p --witness -c your config.conf(Example:/data/java-tron/config.conf) -Example: -java -jar FullNode.jar -p --witness -c /data/java-tron/config.conf - -``` - -This is similar to running a private testnet, except that the IPs in the `config.conf` are officially declared by TRON. - -

-Correct output - -```bash - -20:43:18.138 INFO [main] [o.t.p.FullNode](FullNode.java:21) Full node running. -20:43:18.486 INFO [main] [o.t.c.c.a.Args](Args.java:429) Bind address wasn't set, Punching to identify it... -20:43:18.493 INFO [main] [o.t.c.c.a.Args](Args.java:433) UDP local bound to: 10.0.8.146 -20:43:18.495 INFO [main] [o.t.c.c.a.Args](Args.java:448) External IP wasn't set, using checkip.amazonaws.com to identify it... -20:43:19.450 INFO [main] [o.t.c.c.a.Args](Args.java:461) External address identified: 47.74.147.87 -20:43:19.599 INFO [main] [o.s.c.a.AnnotationConfigApplicationContext](AbstractApplicationContext.java:573) Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@124c278f: startup date [Fri Apr 27 20:43:19 CST 2018]; root of context hierarchy -20:43:19.972 INFO [main] [o.s.b.f.a.AutowiredAnnotationBeanPostProcessor](AutowiredAnnotationBeanPostProcessor.java:153) JSR-330 'javax.inject.Inject' annotation found and supported for autowiring -20:43:20.380 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:244) update latest block header timestamp = 0 -20:43:20.383 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:252) update latest block header number = 0 -20:43:20.393 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:260) update latest block header id = 00 -20:43:20.394 INFO [main] [o.t.c.d.DynamicPropertiesStore](DynamicPropertiesStore.java:265) update state flag = 0 -20:43:20.559 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.567 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.568 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.568 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.569 INFO [main] [o.t.c.c.TransactionCapsule](TransactionCapsule.java:83) Transaction create succeeded! -20:43:20.596 INFO [main] [o.t.c.d.Manager](Manager.java:300) create genesis block -20:43:20.607 INFO [main] [o.t.c.d.Manager](Manager.java:306) save block: BlockCapsule - -``` - -Then observe whether block synchronization success,If synchronization successfully explains the success of the super node - -
- - -### Running a Super Representative Node for private testnet -* use master branch -* You should modify the config.conf - 1. Replace existing entry in genesis.block.witnesses with your address. - 2. Replace existing entry in seed.node ip.list with your ip list. - 3. The first Super Node start, needSyncCheck should be set false - 4. Set p2pversion to 61 - -* Use the executable JAR(Recommended way) - -```bash -cd build/libs -java -jar FullNode.jar -p --witness -c your config.conf (Example:/data/java-tron/config.conf) -Example: -java -jar FullNode.jar -p --witness -c /data/java-tron/config.conf - -``` - -
-Show Output - -```bash -> ./gradlew run -Pwitness - -> Task :generateProto UP-TO-DATE -Using TaskInputs.file() with something that doesn't resolve to a File object has been deprecated and is scheduled to be removed in Gradle 5.0. Use TaskInputs.files() instead. - -> Task :run -20:39:22.749 INFO [o.t.c.c.a.Args] private.key = 63e62a71ed3... -20:39:22.816 WARN [o.t.c.c.a.Args] localwitness size must be one, get the first one -20:39:22.832 INFO [o.t.p.FullNode] Here is the help message.output-directory/ -三月 22, 2018 8:39:23 下午 org.tron.core.services.RpcApiService start -信息: Server started, listening on 50051 -20:39:23.706 INFO [o.t.c.o.n.GossipLocalNode] listener message -20:39:23.712 INFO [o.t.c.o.n.GossipLocalNode] sync group = a41d27f10194c53703be90c6f8735bb66ffc53aa10ea9024d92dbe7324b1aee3 -20:39:23.716 INFO [o.t.c.s.WitnessService] Sleep : 1296 ms,next time:2018-03-22T20:39:25.000+08:00 -20:39:23.734 WARN [i.s.t.BootstrapFactory] Env doesn't support epoll transport -20:39:23.746 INFO [i.s.t.TransportImpl] Bound to: 192.168.10.163:7080 -20:39:23.803 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:25.019 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:25.067+08:00] -20:39:25.019 INFO [o.t.c.s.WitnessService] ScheduledWitness[448d53b2df0cd78158f6f0aecdf60c1c10b15413],slot[1946] -20:39:25.021 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:25.021 INFO [o.t.c.s.WitnessService] Sleep : 4979 ms,next time:2018-03-22T20:39:30.000+08:00 -20:39:30.003 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:30.052+08:00] -20:39:30.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf],slot[1947] -20:39:30.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:30.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:35.000+08:00 -20:39:33.803 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:35.005 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:35.054+08:00] -20:39:35.005 INFO [o.t.c.s.WitnessService] ScheduledWitness[48e447ec869216de76cfeeadf0db37a3d1c8246d],slot[1948] -20:39:35.005 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:35.005 INFO [o.t.c.s.WitnessService] Sleep : 4995 ms,next time:2018-03-22T20:39:40.000+08:00 -20:39:40.005 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T17:57:20.001+08:00],now[2018-03-22T20:39:40.055+08:00] -20:39:40.010 INFO [o.t.c.d.Manager] postponedTrxCount[0],TrxLeft[0] -20:39:40.022 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header id = fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2 -20:39:40.022 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 104, 97, 115, 104], BytesCapsule is org.tron.core.capsule.BytesCapsule@2ce0e954 -20:39:40.023 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header number = 140 -20:39:40.024 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 110, 117, 109, 98, 101, 114], BytesCapsule is org.tron.core.capsule.BytesCapsule@83924ab -20:39:40.024 INFO [o.t.c.d.DynamicPropertiesStore] update latest block header timestamp = 1521722380001 -20:39:40.024 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 97, 116, 101, 115, 116, 95, 98, 108, 111, 99, 107, 95, 104, 101, 97, 100, 101, 114, 95, 116, 105, 109, 101, 115, 116, 97, 109, 112], BytesCapsule is org.tron.core.capsule.BytesCapsule@ca6a6f8 -20:39:40.024 INFO [o.t.c.d.Manager] updateWitnessSchedule number:140,HeadBlockTimeStamp:1521722380001 -20:39:40.025 WARN [o.t.c.u.RandomGenerator] index[-3] is out of range[0,3],skip -20:39:40.070 INFO [o.t.c.d.TronStoreWithRevoking] Address is [73, 72, -62, -24, -89, 86, -39, 67, 112, 55, -36, -40, -57, -32, -57, 61, 86, 12, -93, -115], AccountCapsule is account_name: "Sun" -address: "IH\302\350\247V\331Cp7\334\330\307\340\307=V\f\243\215" -balance: 9223372036854775387 - -20:39:40.081 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], AccountCapsule is type: AssetIssue -address: ")\237=\270\n$\262\n%K\211\316c\235Y\023/\025\177\023" -balance: 420 - -20:39:40.082 INFO [o.t.c.d.TronStoreWithRevoking] Address is [76, 65, 84, 69, 83, 84, 95, 83, 79, 76, 73, 68, 73, 70, 73, 69, 68, 95, 66, 76, 79, 67, 75, 95, 78, 85, 77], BytesCapsule is org.tron.core.capsule.BytesCapsule@ec1439 -20:39:40.083 INFO [o.t.c.d.Manager] there is account List size is 8 -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 448d53b2df0cd78158f6f0aecdf60c1c10b15413 -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 548794500882809695a8a687866e76d4271a146a -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 48e447ec869216de76cfeeadf0db37a3d1c8246d -20:39:40.084 INFO [o.t.c.d.Manager] there is account ,account address is 55ddae14564f82d5b94c7a131b5fcfd31ad6515a -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 299f3db80a24b20a254b89ce639d59132f157f13 -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is abd4b9367799eaa3197fecb144eb71de1e049150 -20:39:40.085 INFO [o.t.c.d.Manager] there is account ,account address is 4948c2e8a756d9437037dcd8c7e0c73d560ca38d -20:39:40.085 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 34, -63, -81, 123, -5, -78, -80, -32, 113, 72, -20, -70, 39, -75, 111, -127, -91, 79, -49], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@4cb4f7fb -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@7be2474a -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [72, -28, 71, -20, -122, -110, 22, -34, 118, -49, -18, -83, -16, -37, 55, -93, -47, -56, 36, 109], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@3e375891 -20:39:40.086 INFO [o.t.c.d.TronStoreWithRevoking] Address is [68, -115, 83, -78, -33, 12, -41, -127, 88, -10, -16, -82, -51, -10, 12, 28, 16, -79, 84, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@55d77b83 -20:39:40.090 INFO [o.t.c.d.Manager] countWitnessMap size is 0 -20:39:40.091 INFO [o.t.c.d.TronStoreWithRevoking] Address is [41, -97, 61, -72, 10, 36, -78, 10, 37, 75, -119, -50, 99, -99, 89, 19, 47, 21, 127, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@310dd876 -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [72, -28, 71, -20, -122, -110, 22, -34, 118, -49, -18, -83, -16, -37, 55, -93, -47, -56, 36, 109], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@151b42bc -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [108, 34, -63, -81, 123, -5, -78, -80, -32, 113, 72, -20, -70, 39, -75, 111, -127, -91, 79, -49], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@2d0388aa -20:39:40.092 INFO [o.t.c.d.TronStoreWithRevoking] Address is [68, -115, 83, -78, -33, 12, -41, -127, 88, -10, -16, -82, -51, -10, 12, 28, 16, -79, 84, 19], WitnessCapsule is org.tron.core.capsule.WitnessCapsule@478a55e7 -20:39:40.101 INFO [o.t.c.d.TronStoreWithRevoking] Address is [-3, 48, -95, 97, 96, 113, 95, 60, -95, -91, -68, -83, 24, -24, 25, -111, -51, 111, 71, 38, 90, 113, -127, 91, -46, -55, 67, 18, -101, 37, -116, -46], BlockCapsule is BlockCapsule{blockId=fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2, num=140, parentId=dadeff07c32d342b941cfa97ba82870958615e7ae73fffeaf3c6a334d81fe3bd, generatedByMyself=true} -20:39:40.102 INFO [o.t.c.d.Manager] save block: BlockCapsule{blockId=fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2, num=140, parentId=dadeff07c32d342b941cfa97ba82870958615e7ae73fffeaf3c6a334d81fe3bd, generatedByMyself=true} -20:39:40.102 INFO [o.t.c.s.WitnessService] Block is generated successfully, Its Id is fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2,number140 -20:39:40.102 INFO [o.t.c.n.n.NodeImpl] Ready to broadcast a block, Its hash is fd30a16160715f3ca1a5bcad18e81991cd6f47265a71815bd2c943129b258cd2 -20:39:40.107 INFO [o.t.c.s.WitnessService] Produced -20:39:40.107 INFO [o.t.c.s.WitnessService] Sleep : 4893 ms,next time:2018-03-22T20:39:45.000+08:00 -20:39:43.805 INFO [o.t.c.n.n.NodeImpl] other peer is nil, please wait ... -20:39:45.002 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T20:39:45.001+08:00],now[2018-03-22T20:39:45.052+08:00] -20:39:45.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[48e447ec869216de76cfeeadf0db37a3d1c8246d],slot[1] -20:39:45.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:45.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:50.000+08:00 -20:39:50.002 WARN [o.t.c.d.Manager] nextFirstSlotTime:[2018-03-22T20:39:45.001+08:00],now[2018-03-22T20:39:50.052+08:00] -20:39:50.003 INFO [o.t.c.s.WitnessService] ScheduledWitness[6c22c1af7bfbb2b0e07148ecba27b56f81a54fcf],slot[2] -20:39:50.003 INFO [o.t.c.s.WitnessService] It's not my turn -20:39:50.003 INFO [o.t.c.s.WitnessService] Sleep : 4997 ms,next time:2018-03-22T20:39:55.000+08:00 - -``` - -
- -* In IntelliJ IDEA - -
- - -Open the configuration panel: - - - -![](docs/images/program_configure.png) - -
- -
- - -In the `Program arguments` option, fill in `--witness`: - - - -![](docs/images/set_witness_param.jpeg) - -
- -Then, run `FullNode::main()` again. - -## Advanced Configurations - -Read the [Advanced Configurations](common/src/main/java/org/tron/core/config/README.md).