diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef800e15ff..dd005f98b74 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -54,6 +54,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + build-ubuntu: name: Build ubuntu24 (JDK 17 / aarch64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'ubuntu' }} @@ -84,6 +93,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + docker-build-rockylinux: name: Build rockylinux (JDK 8 / x86_64) if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'rockylinux' }} @@ -127,6 +145,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon @@ -172,6 +199,15 @@ jobs: - name: Build run: ./gradlew clean build --no-daemon --no-build-cache + - name: Toolkit jar smoke test + run: | + set -e + JAR=plugins/build/libs/Toolkit.jar + java -jar "$JAR" help + java -jar "$JAR" db --help + java -jar "$JAR" db archive -h + java -jar "$JAR" keystore --help + - name: Test with RocksDB engine run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache @@ -279,6 +315,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 +386,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 +399,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 +407,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 +429,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 +440,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 +461,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 +500,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/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index cd42d7a9010..6b297883fbd 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -886,6 +886,22 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]"); + } + if (dynamicPropertiesStore.getAllowHardenResourceCalculation() == 1) { + throw new ContractValidateException( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, " + + "no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1"); + } + break; + } default: break; } @@ -971,7 +987,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_BLOB(89), // 0, 1 PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 - ALLOW_TVM_OSAKA(96); // 0, 1 + ALLOW_TVM_OSAKA(96), // 0, 1 + ALLOW_HARDEN_RESOURCE_CALCULATION(97); // 0, 1 private long code; diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index e099101912b..881eb861bea 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -46,6 +46,7 @@ public static void load(StoreFactory storeFactory) { VMConfig.initAllowTvmBlob(ds.getAllowTvmBlob()); VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); + VMConfig.initAllowHardenResourceCalculation(ds.getAllowHardenResourceCalculation()); } } } diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 9de7c0691ba..62e7ce6ec08 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -8,6 +8,7 @@ import com.google.common.collect.HashBasedTable; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.util.HashMap; import java.util.HashSet; import java.util.Optional; @@ -17,6 +18,7 @@ import org.bouncycastle.util.Strings; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; @@ -223,7 +225,7 @@ public Pair getAccountEnergyUsageBalanceAndRestoreSeconds(AccountCap long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getTotalEnergyWeight(); - long balance = (long) ((double) newEnergyUsage * totalEnergyWeight / totalEnergyLimit * TRX_PRECISION); + long balance = usageToBalance(newEnergyUsage, totalEnergyWeight, totalEnergyLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } @@ -246,11 +248,22 @@ public Pair getAccountNetUsageBalanceAndRestoreSeconds(AccountCapsul long totalNetLimit = getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = getTotalNetWeight(); - long balance = (long) ((double) newNetUsage * totalNetWeight / totalNetLimit * TRX_PRECISION); + long balance = usageToBalance(newNetUsage, totalNetWeight, totalNetLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } + private long usageToBalance(long usage, long totalWeight, long totalLimit) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(totalWeight)) + .multiply(BigInteger.valueOf(TRX_PRECISION)) + .divide(BigInteger.valueOf(totalLimit)) + .longValueExact(); + } + return (long) ((double) usage * totalWeight / totalLimit * TRX_PRECISION); + } + @Override public AssetIssueCapsule getAssetIssue(byte[] tokenId) { byte[] tokenIdWithoutLeadingZero = ByteUtil.stripLeadingZeroes(tokenId); @@ -896,8 +909,19 @@ private long recover(long lastUsage, long lastTime, long now, long personalWindo } private long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenResourceCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -917,21 +941,46 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)) + .longValueExact(); + } return usage * windowSize / precision; } + private boolean hardenResourceCalculation() { + return VMConfig.allowHardenResourceCalculation(); + } + public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { long frozeBalance = accountCapsule.getAllFrozenBalanceForEnergy(); - if (frozeBalance < 1_000_000L) { + if (frozeBalance < TRX_PRECISION) { return 0; } - long energyWeight = frozeBalance / 1_000_000L; + long energyWeight = frozeBalance / TRX_PRECISION; long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getDynamicPropertiesStore().getTotalEnergyWeight(); assert totalEnergyWeight > 0; + if (hardenResourceCalculation()) { + return BigInteger.valueOf(energyWeight) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + } return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } diff --git a/build.gradle b/build.gradle index 12a0622db99..35bc557a583 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ ext.archInfo = [ // https://github.com/grpc/grpc-java/issues/7690 // https://github.com/grpc/grpc-java/pull/12319, Add support for macOS aarch64 with universal binary // https://github.com/grpc/grpc-java/pull/11371 , 1.64.x is not supported CentOS 7. - ProtocGenVersion: isArm64 && isMac ? '1.76.0' : '1.60.0' + ProtocGenVersion: isArm64 || isMac ? '1.81.0' : '1.60.0' ], VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions" ] @@ -81,8 +81,9 @@ subprojects { } dependencies { - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' - implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.25' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' + implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36' + implementation group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.36' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.13' implementation "com.google.code.findbugs:jsr305:3.0.0" implementation group: 'org.springframework', name: 'spring-context', version: "${springVersion}" @@ -90,7 +91,7 @@ subprojects { implementation group: 'org.apache.commons', name: 'commons-math', version: '2.2' implementation "org.apache.commons:commons-collections4:4.1" implementation group: 'joda-time', name: 'joda-time', version: '2.3' - implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.79' + implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.84' compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34' 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/common/storage/leveldb/LevelDbDataSourceImpl.java b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java index c48800573e1..aa85ac08f45 100644 --- a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java +++ b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java @@ -32,6 +32,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -45,6 +48,7 @@ import org.iq80.leveldb.ReadOptions; import org.iq80.leveldb.WriteBatch; import org.iq80.leveldb.WriteOptions; +import org.tron.common.es.ExecutorServiceManager; import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.WriteOptionsWrapper; import org.tron.common.storage.metric.DbStat; @@ -61,6 +65,11 @@ public class LevelDbDataSourceImpl extends DbStat implements DbSourceInter, Iterable>, Instance { + /** First watchdog WARN fires this many seconds after factory.open() begins. */ + private static final long OPEN_WATCHDOG_INITIAL_DELAY_SEC = 60; + /** Subsequent watchdog WARN lines are emitted on this interval. */ + private static final long OPEN_WATCHDOG_PERIOD_SEC = 30; + private String dataBaseName; private DB database; private volatile boolean alive; @@ -121,6 +130,14 @@ private void openDatabase(Options dbOptions) throws IOException { if (!Files.isSymbolicLink(dbPath.getParent())) { Files.createDirectories(dbPath.getParent()); } + final long openStartNs = System.nanoTime(); + ScheduledExecutorService watchdog = ExecutorServiceManager + .newSingleThreadScheduledExecutor("db-open-watchdog-" + dataBaseName, true); + ScheduledFuture watchdogTask = watchdog.scheduleAtFixedRate( + () -> logSlowOpen(dbPath, openStartNs), + OPEN_WATCHDOG_INITIAL_DELAY_SEC, + OPEN_WATCHDOG_PERIOD_SEC, + TimeUnit.SECONDS); try { DbSourceInter.checkOrInitEngine(getEngine(), dbPath.toString(), TronError.ErrCode.LEVELDB_INIT); @@ -139,6 +156,28 @@ private void openDatabase(Options dbOptions) throws IOException { logger.error("Open Database {} failed", dataBaseName, e); } throw new TronError(e, TronError.ErrCode.LEVELDB_INIT); + } finally { + watchdogTask.cancel(false); + watchdog.shutdownNow(); + } + } + + /** + * Emits a WARN when factory.open() is still blocked — usually because the + * MANIFEST has grown large enough to make replay expensive. + */ + void logSlowOpen(Path dbPath, long startNs) { + try { + long elapsedSec = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNs); + logger.warn("DB {} open still in progress after {}s. path={}. " + + "This startup will complete; to speed up future restarts, run " + + "`java -jar Toolkit.jar db archive -d {}` before the next startup " + + "to rebuild the MANIFEST (the tool requires an exclusive DB lock, " + + "so it cannot run while the node is up).", + dataBaseName, elapsedSec, dbPath, parentPath); + } catch (Exception e) { + // Purely observational - never let the watchdog disrupt startup. + logger.debug("db-open-watchdog failure for {}: {}", dataBaseName, e.getMessage()); } } diff --git a/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java b/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java index c7fecf2a351..eb0362ad2e9 100644 --- a/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java +++ b/chainbase/src/main/java/org/tron/common/storage/metric/DbStat.java @@ -17,7 +17,7 @@ protected void statProperty() { double size = Double.parseDouble(tmp[2]) * 1048576.0; Metrics.gaugeSet(MetricKeys.Gauge.DB_SST_LEVEL, files, getEngine(), getName(), level); Metrics.gaugeSet(MetricKeys.Gauge.DB_SIZE_BYTES, size, getEngine(), getName(), level); - logger.info("DB {}, level:{},files:{},size:{} M", + logger.debug("DB {}, level:{},files:{},size:{} M", getName(), level, files, size / 1048576.0); }); } catch (Exception e) { diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java index b11c6b1e0a4..bb4b70cde1b 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import lombok.Getter; import lombok.Setter; @@ -94,6 +95,8 @@ public class TransactionCapsule implements ProtoCapsule { .newFixedThreadPool(esName, CommonParameter.getInstance() .getValidContractProtoThreadNum()); private static final String OWNER_ADDRESS = "ownerAddress_"; + // 2-6 ms in general, so we set 50 ms as the threshold for slow signature verification. + private static final long SLOW_SIG_VERIFY_MS = 50; private Transaction transaction; @Setter @@ -648,6 +651,7 @@ public boolean validatePubSignature(AccountStore accountStore, byte[] hash = getTransactionId().getBytes(); + long startNs = System.nanoTime(); try { if (!validateSignature(this.transaction, hash, accountStore, dynamicPropertiesStore)) { isVerified = false; @@ -656,12 +660,27 @@ public boolean validatePubSignature(AccountStore accountStore, } catch (SignatureException | PermissionException | SignatureFormatException e) { isVerified = false; throw new ValidateSignatureException(e.getMessage()); + } finally { + logSlowSigVerify(startNs); } isVerified = true; } return true; } + /** + * WARN-logs when a single signature verification exceeds + * {@link #SLOW_SIG_VERIFY_MS}. Package-private so it can be exercised from + * tests without forcing a real slow crypto path. + */ + void logSlowSigVerify(long startNs) { + long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + if (costMs > SLOW_SIG_VERIFY_MS) { + logger.warn("slow verify: txId={}, sigCount={}, cost={} ms", + getTransactionId(), this.transaction.getSignatureCount(), costMs); + } + } + /** * validate signature */ diff --git a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java index 2488686bfb0..ece16b25819 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -437,7 +437,6 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (frozeBalance < TRX_PRECISION) { return 0; } - long netWeight = frozeBalance / TRX_PRECISION; long totalNetLimit = chainBaseManager.getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = chainBaseManager.getDynamicPropertiesStore().getTotalNetWeight(); if (dynamicPropertiesStore.allowNewReward() && totalNetWeight <= 0) { @@ -446,16 +445,23 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalNetLimit, totalNetWeight); + } + long netWeight = frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } public long calculateGlobalNetLimitV2(long frozeBalance) { - double netWeight = (double) frozeBalance / TRX_PRECISION; long totalNetLimit = dynamicPropertiesStore.getTotalNetLimit(); long totalNetWeight = dynamicPropertiesStore.getTotalNetWeight(); if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalNetLimit, totalNetWeight); + } + double netWeight = (double) frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } diff --git a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java index 30d778d0990..0c429178636 100644 --- a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java @@ -5,6 +5,7 @@ import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; import org.tron.common.parameter.CommonParameter; import org.tron.core.capsule.AccountCapsule; @@ -71,17 +72,20 @@ public void updateAdaptiveTotalEnergyLimit() { long result; if (totalEnergyAverageUsage > targetTotalEnergyLimit) { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + ">" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR); } else { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + "<" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR); } + long upperBound = hardenCalculation() + ? BigInteger.valueOf(totalEnergyLimit).multiply(BigInteger.valueOf( + dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier())).longValueExact() + : totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(); result = min(max(result, totalEnergyLimit, this.disableJavaLangMath()), - totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(), - this.disableJavaLangMath()); + upperBound, this.disableJavaLangMath()); dynamicPropertiesStore.saveTotalEnergyCurrentLimit(result); logger.debug("Adjust totalEnergyCurrentLimit, old: {}, new: {}.", @@ -147,7 +151,6 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { return 0; } - long energyWeight = frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (dynamicPropertiesStore.allowNewReward() && totalEnergyWeight <= 0) { @@ -155,16 +158,23 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { } else { assert totalEnergyWeight > 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + long energyWeight = frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } public long calculateGlobalEnergyLimitV2(long frozeBalance) { - double energyWeight = (double) frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (totalEnergyWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + double energyWeight = (double) frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } @@ -184,7 +194,15 @@ private long getHeadSlot() { return getHeadSlot(dynamicPropertiesStore); } - + private long scaleByRate(long value, long numerator, long denominator) { + if (hardenCalculation()) { + return BigInteger.valueOf(value) + .multiply(BigInteger.valueOf(numerator)) + .divide(BigInteger.valueOf(denominator)) + .longValueExact(); + } + return value * numerator / denominator; + } } diff --git a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java index 7e170f9dab5..6706c430084 100644 --- a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java @@ -3,8 +3,11 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.math.Maths.round; import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; import static org.tron.core.config.Parameter.ChainConstant.WINDOW_SIZE_PRECISION; +import java.math.BigInteger; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.Commons; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.TransactionCapsule; @@ -45,8 +48,19 @@ protected long increase(long lastUsage, long usage, long lastTime, long now) { } protected long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -75,8 +89,20 @@ public long increase(AccountCapsule accountCapsule, ResourceCode resourceCode, return increaseV2(accountCapsule, resourceCode, lastUsage, usage, lastTime, now); } long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -108,8 +134,20 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, long lastUsage, long usage, long lastTime, long now) { long oldWindowSizeV2 = accountCapsule.getWindowSizeV2(resourceCode); long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -130,8 +168,19 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, } long remainWindowSize = oldWindowSizeV2 - (now - lastTime) * WINDOW_SIZE_PRECISION; - long newWindowSize = divideCeil( - remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, newUsage); + long newWindowSize; + if (hardenCalculation()) { + BigInteger biNewWindowSize = BigInteger.valueOf(remainUsage) + .multiply(BigInteger.valueOf(remainWindowSize)) + .add(BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(this.windowSize)) + .multiply(BigInteger.valueOf(WINDOW_SIZE_PRECISION))); + newWindowSize = divideCeilExact(biNewWindowSize, BigInteger.valueOf(newUsage)); + } else { + newWindowSize = divideCeil( + remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, + newUsage); + } newWindowSize = min(newWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); accountCapsule.setNewWindowSizeV2(resourceCode, newWindowSize); @@ -191,10 +240,18 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece remainReceiverWindowSizeV2 = remainReceiverWindowSizeV2 < 0 ? 0 : remainReceiverWindowSizeV2; // calculate new windowSize - long newOwnerWindowSize = - divideCeil( - ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, - newOwnerUsage); + long newOwnerWindowSize; + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(ownerUsage) + .multiply(BigInteger.valueOf(remainOwnerWindowSizeV2)) + .add(BigInteger.valueOf(transferUsage) + .multiply(BigInteger.valueOf(remainReceiverWindowSizeV2))); + newOwnerWindowSize = divideCeilExact(bi, BigInteger.valueOf(newOwnerUsage)); + } else { + newOwnerWindowSize = divideCeil( + ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, + newOwnerUsage); + } newOwnerWindowSize = min(newOwnerWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); owner.setNewWindowSizeV2(resourceCode, newOwnerWindowSize); @@ -204,6 +261,11 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece private long getNewWindowSize(long lastUsage, long lastWindowSize, long usage, long windowSize, long newUsage) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(lastUsage).multiply(BigInteger.valueOf(lastWindowSize)) + .add(BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize))); + return bi.divide(BigInteger.valueOf(newUsage)).longValueExact(); + } return (lastUsage * lastWindowSize + usage * windowSize) / newUsage; } @@ -211,11 +273,29 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenCalculation()) { + return BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)).longValueExact(); + } return usage * windowSize / precision; } private long getUsage(long oldUsage, long oldWindowSize, long newUsage, long newWindowSize) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(oldUsage).multiply(BigInteger.valueOf(oldWindowSize)) + .add(BigInteger.valueOf(newUsage).multiply(BigInteger.valueOf(newWindowSize))); + return bi.divide(BigInteger.valueOf(precision)).longValueExact(); + } return (oldUsage * oldWindowSize + newUsage * newWindowSize) / precision; } @@ -262,4 +342,38 @@ protected boolean consumeFeeForNewAccount(AccountCapsule accountCapsule, long fe protected boolean disableJavaLangMath() { return dynamicPropertiesStore.disableJavaLangMath(); } + + protected boolean hardenCalculation() { + return dynamicPropertiesStore.allowHardenResourceCalculation(); + } + + protected long calculateGlobalLimitV1(long frozeBalance, + long totalLimit, long totalWeight) { + long weight = frozeBalance / TRX_PRECISION; + return BigInteger.valueOf(weight) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(totalWeight)) + .longValueExact(); + } + + /** + * Hardened replacement of legacy V2 formula + * {@code (long)(((double) frozeBalance / TRX_PRECISION) + * * ((double) totalLimit / totalWeight))}. + * + *

Preserves V2 semantics: equivalent to + * {@code (frozeBalance * totalLimit) / (TRX_PRECISION * totalWeight)} with + * a single integer truncation at the end. Critically, fractional weight + * (i.e. {@code frozeBalance < TRX_PRECISION}) is preserved through the + * multiplication and only truncated at the final divide, so small balances + * yield the same proportional result as the double-arithmetic path. + */ + protected long calculateGlobalLimitV2(long frozeBalance, + long totalLimit, long totalWeight) { + return BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(TRX_PRECISION) + .multiply(BigInteger.valueOf(totalWeight))) + .longValueExact(); + } } 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/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index e0adb0d444a..12dfca5e59a 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] ALLOW_HARDEN_RESOURCE_CALCULATION = + "ALLOW_HARDEN_RESOURCE_CALCULATION".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2996,21 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public long getAllowHardenResourceCalculation() { + return Optional.ofNullable(getUnchecked(ALLOW_HARDEN_RESOURCE_CALCULATION)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowHardenResourceCalculation(long value) { + this.put(ALLOW_HARDEN_RESOURCE_CALCULATION, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowHardenResourceCalculation() { + return getAllowHardenResourceCalculation() == 1L; + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); diff --git a/common/src/main/java/org/tron/common/log/LogService.java b/common/src/main/java/org/tron/common/log/LogService.java index bce52001e92..fdeba534b5e 100644 --- a/common/src/main/java/org/tron/common/log/LogService.java +++ b/common/src/main/java/org/tron/common/log/LogService.java @@ -2,6 +2,8 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.jul.LevelChangePropagator; +import ch.qos.logback.classic.spi.LoggerContextListener; import ch.qos.logback.core.util.StatusPrinter; import java.io.File; import org.slf4j.LoggerFactory; @@ -12,18 +14,43 @@ public class LogService { public static void load(String path) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); try { - File file = new File(path); - if (!file.exists() || !file.isFile() || !file.canRead()) { - return; + // Fail fast rather than silently falling back to the classpath default — + // that legacy behavior misled operators into thinking their custom + // --log-config was active. + if (path != null && !path.isEmpty()) { + File file = new File(path); + if (!file.exists() || !file.isFile() || !file.canRead()) { + throw new IllegalArgumentException( + "logback config is not a readable file: " + path); + } + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(lc); + lc.reset(); + configurator.doConfigure(file); } - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(file); + // Whether we loaded a custom config via --log-config or kept the classpath + // default, make sure Logback level changes are propagated back to JUL so + // gRPC loggers actually honor the levels declared in the XML. If + // the active config already registered a LevelChangePropagator we leave + // it alone. + ensureLevelChangePropagator(lc); } catch (Exception e) { throw new TronError(e, TronError.ErrCode.LOG_LOAD); } finally { StatusPrinter.printInCaseOfErrorsOrWarnings(lc); } } + + private static void ensureLevelChangePropagator(LoggerContext lc) { + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + return; + } + } + LevelChangePropagator propagator = new LevelChangePropagator(); + propagator.setContext(lc); + propagator.setResetJUL(true); + propagator.start(); + lc.addListener(propagator); + } } 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..1bb61c420f0 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -8,6 +8,7 @@ import java.util.Set; import lombok.Getter; import lombok.Setter; +import org.slf4j.bridge.SLF4JBridgeHandler; import org.tron.common.args.GenesisBlock; import org.tron.common.config.DbBackupConfig; import org.tron.common.cron.CronExpression; @@ -23,6 +24,19 @@ public class CommonParameter { + // Install the JUL->SLF4J bridge early so that JUL log records emitted during + // static init of grpc classes (or from unit tests that don't invoke + // LogService.load()) still reach Logback. + // removeHandlersForRootLogger() strips JUL's default ConsoleHandler so the + // same record is not emitted twice (once by JUL's own console output and + // once via the bridge to Logback). + static { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + if (!SLF4JBridgeHandler.isInstalled()) { + SLF4JBridgeHandler.install(); + } + } + protected static CommonParameter PARAMETER = new CommonParameter(); // Runtime chain state: set by VMConfig.initVmHardFork() @@ -119,6 +133,9 @@ public class CommonParameter { public int maxTps; // clearParam: 1000 @Getter @Setter + public int maxBlockInvPerSecond = 10; // default: 10 block inv hashes/s per peer + @Getter + @Setter public int minParticipationRate; @Getter public P2pConfig p2pConfig; @@ -211,11 +228,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 @@ -335,7 +360,7 @@ public class CommonParameter { @Getter @Setter - public boolean allowShieldedTransactionApi; // clearParam: true + public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter public long blockNumForEnergyLimit; 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/common/prometheus/MetricKeys.java b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java index 87ab6fae0a3..95a38c4b479 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java @@ -14,6 +14,10 @@ public static class Counter { public static final String TXS = "tron:txs"; public static final String MINER = "tron:miner"; public static final String BLOCK_FORK = "tron:block_fork"; + // witness label: bounded cardinality -- SR candidate pool is finite, rotation is + // infrequent (at most once per maintenance interval); kept for at-a-glance SR + // identification in dashboards rather than requiring log cross-referencing. + public static final String SR_SET_CHANGE = "tron:sr_set_change"; public static final String P2P_ERROR = "tron:p2p_error"; public static final String P2P_DISCONNECT = "tron:p2p_disconnect"; public static final String INTERNAL_SERVICE_FAIL = "tron:internal_service_fail"; @@ -62,6 +66,7 @@ public static class Histogram { public static final String MESSAGE_PROCESS_LATENCY = "tron:message_process_latency_seconds"; public static final String BLOCK_FETCH_LATENCY = "tron:block_fetch_latency_seconds"; public static final String BLOCK_RECEIVE_DELAY = "tron:block_receive_delay_seconds"; + public static final String BLOCK_TRANSACTION_COUNT = "tron:block_transaction_count"; private Histogram() { throw new IllegalStateException("Histogram"); diff --git a/common/src/main/java/org/tron/common/prometheus/MetricLabels.java b/common/src/main/java/org/tron/common/prometheus/MetricLabels.java index 2aa3c1e3378..1f0da214085 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricLabels.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricLabels.java @@ -31,6 +31,8 @@ public static class Counter { public static final String TXS_FAIL_SIG = "sig"; public static final String TXS_FAIL_TAPOS = "tapos"; public static final String TXS_FAIL_DUP = "dup"; + public static final String SR_ADD = "add"; + public static final String SR_REMOVE = "remove"; private Counter() { throw new IllegalStateException("Counter"); @@ -66,6 +68,7 @@ private Gauge() { // Histogram public static class Histogram { + public static final String MINER = "miner"; public static final String TRAFFIC_IN = "in"; public static final String TRAFFIC_OUT = "out"; diff --git a/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java b/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java index 6acdf23b3bc..7231baaba8f 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricsCounter.java @@ -14,6 +14,7 @@ class MetricsCounter { init(MetricKeys.Counter.TXS, "tron txs info .", "type", "detail"); init(MetricKeys.Counter.MINER, "tron miner info .", "miner", "type"); init(MetricKeys.Counter.BLOCK_FORK, "tron block fork info .", "type"); + init(MetricKeys.Counter.SR_SET_CHANGE, "tron sr set change .", "action", "witness"); init(MetricKeys.Counter.P2P_ERROR, "tron p2p error info .", "type"); init(MetricKeys.Counter.P2P_DISCONNECT, "tron p2p disconnect .", "type"); init(MetricKeys.Counter.INTERNAL_SERVICE_FAIL, "internal Service fail.", diff --git a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java index 556db10feb5..6a66dc76bb3 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java @@ -20,7 +20,7 @@ public class MetricsHistogram { init(MetricKeys.Histogram.JSONRPC_SERVICE_LATENCY, "JsonRpc Service latency.", "method"); init(MetricKeys.Histogram.MINER_LATENCY, "miner latency.", - "miner"); + MetricLabels.Histogram.MINER); init(MetricKeys.Histogram.PING_PONG_LATENCY, "node ping pong latency."); init(MetricKeys.Histogram.VERIFY_SIGN_LATENCY, "verify sign latency for trx , block.", "type"); @@ -36,7 +36,7 @@ public class MetricsHistogram { init(MetricKeys.Histogram.PROCESS_TRANSACTION_LATENCY, "process transaction latency.", "type", "contract"); init(MetricKeys.Histogram.MINER_DELAY, "miner delay time, actualTime - planTime.", - "miner"); + MetricLabels.Histogram.MINER); init(MetricKeys.Histogram.UDP_BYTES, "udp_bytes traffic.", "type"); init(MetricKeys.Histogram.TCP_BYTES, "tcp_bytes traffic.", @@ -48,6 +48,11 @@ public class MetricsHistogram { init(MetricKeys.Histogram.BLOCK_FETCH_LATENCY, "fetch block latency."); init(MetricKeys.Histogram.BLOCK_RECEIVE_DELAY, "receive block delay time, receiveTime - blockTime."); + + init(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, + "Distribution of transaction counts per block.", + new double[]{0, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000}, + MetricLabels.Histogram.MINER); } private MetricsHistogram() { @@ -62,6 +67,17 @@ private static void init(String name, String help, String... labels) { .register()); } + private static void init(String name, String help, double[] buckets, String... labels) { + Histogram.Builder builder = Histogram.build() + .name(name) + .help(help) + .labelNames(labels); + if (buckets != null && buckets.length > 0) { + builder.buckets(buckets); + } + container.put(name, builder.register()); + } + static Histogram.Timer startTimer(String key, String... labels) { if (Metrics.enabled()) { Histogram histogram = container.get(key); diff --git a/common/src/main/java/org/tron/common/prometheus/SRMetrics.java b/common/src/main/java/org/tron/common/prometheus/SRMetrics.java new file mode 100644 index 00000000000..0c547a38e2c --- /dev/null +++ b/common/src/main/java/org/tron/common/prometheus/SRMetrics.java @@ -0,0 +1,26 @@ +package org.tron.common.prometheus; + +import com.google.protobuf.ByteString; +import java.util.List; +import org.tron.common.utils.StringUtil; + +public class SRMetrics { + + private SRMetrics() { + throw new IllegalStateException("SRMetrics"); + } + + public static void recordSrSetChange(List currentWits, List newWits) { + if (!Metrics.enabled()) { + return; + } + newWits.stream() + .filter(w -> !currentWits.contains(w)) + .forEach(w -> Metrics.counterInc(MetricKeys.Counter.SR_SET_CHANGE, 1, + MetricLabels.Counter.SR_ADD, StringUtil.encode58Check(w.toByteArray()))); + currentWits.stream() + .filter(w -> !newWits.contains(w)) + .forEach(w -> Metrics.counterInc(MetricKeys.Counter.SR_SET_CHANGE, 1, + MetricLabels.Counter.SR_REMOVE, StringUtil.encode58Check(w.toByteArray()))); + } +} 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..751fb81e4a1 --- /dev/null +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -0,0 +1,542 @@ +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; + private int maxBlockInvPerSecond = 10; + // Config key "isOpenFullTcpDisconnect" cannot auto-bind — read manually in fromConfig() + @Getter(lombok.AccessLevel.NONE) + @Setter(lombok.AccessLevel.NONE) + private boolean isOpenFullTcpDisconnect = false; + + public boolean isOpenFullTcpDisconnect() { return isOpenFullTcpDisconnect; } + + // 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 = false; + 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; + } + + // maxBlockInvPerSecond: minimum 1 + if (maxBlockInvPerSecond < 1) { + maxBlockInvPerSecond = 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/java/org/tron/core/exception/ZksnarkException.java b/common/src/main/java/org/tron/core/exception/ZksnarkException.java index ec75e03852b..fab8019aebf 100644 --- a/common/src/main/java/org/tron/core/exception/ZksnarkException.java +++ b/common/src/main/java/org/tron/core/exception/ZksnarkException.java @@ -9,4 +9,8 @@ public ZksnarkException() { public ZksnarkException(String message) { super(message); } + + public ZksnarkException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 1a7f0c058a4..94c1e50284e 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -63,6 +63,8 @@ public class VMConfig { private static boolean ALLOW_TVM_OSAKA = false; + private static boolean ALLOW_HARDEN_RESOURCE_CALCULATION = false; + private VMConfig() { } @@ -178,6 +180,10 @@ public static void initAllowTvmOsaka(long allow) { ALLOW_TVM_OSAKA = allow == 1; } + public static void initAllowHardenResourceCalculation(long allow) { + ALLOW_HARDEN_RESOURCE_CALCULATION = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -281,4 +287,8 @@ public static boolean allowTvmSelfdestructRestriction() { public static boolean allowTvmOsaka() { return ALLOW_TVM_OSAKA; } + + public static boolean allowHardenResourceCalculation() { + return ALLOW_HARDEN_RESOURCE_CALCULATION; + } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf new file mode 100644 index 00000000000..7bf1d24da5a --- /dev/null +++ b/common/src/main/resources/reference.conf @@ -0,0 +1,843 @@ +# ============================================================================= +# 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 = false + + # 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 + # Max block inv hashes accepted per peer per second. Minimum: 1. + maxBlockInvPerSecond = 10 + + 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..a52c51c1ba4 --- /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 testShieldedApiDefaultsToFalseWhenNeitherKeySet() { + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertFalse(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiModernKeyRespected() { + NodeConfig nc = NodeConfig.fromConfig( + withRef("node.allowShieldedTransactionApi = true")); + assertTrue(nc.isAllowShieldedTransactionApi()); + } + + @Test + public void testShieldedApiLegacyKeyRespected() { + // Regression guard: reference.conf ships `allowShieldedTransactionApi = false`, 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 = true")); + assertTrue(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/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java new file mode 100644 index 00000000000..26fa8fdd99a --- /dev/null +++ b/common/src/test/java/org/tron/core/exception/ZksnarkExceptionTest.java @@ -0,0 +1,29 @@ +package org.tron.core.exception; + +import org.junit.Assert; +import org.junit.Test; + +public class ZksnarkExceptionTest { + + @Test + public void testNoArgConstructor() { + ZksnarkException e = new ZksnarkException(); + Assert.assertNull(e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageConstructor() { + ZksnarkException e = new ZksnarkException("boom"); + Assert.assertEquals("boom", e.getMessage()); + Assert.assertNull(e.getCause()); + } + + @Test + public void testMessageAndCauseConstructor() { + Throwable cause = new ArithmeticException("overflow"); + ZksnarkException e = new ZksnarkException("wrapped", cause); + Assert.assertEquals("wrapped", e.getMessage()); + Assert.assertSame(cause, e.getCause()); + } +} diff --git a/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java b/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java index 012169bdb87..fd5e4364d0d 100644 --- a/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java +++ b/consensus/src/main/java/org/tron/consensus/dpos/MaintenanceManager.java @@ -16,6 +16,7 @@ import org.bouncycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.tron.common.prometheus.SRMetrics; import org.tron.consensus.ConsensusDelegate; import org.tron.consensus.pbft.PbftManager; import org.tron.core.capsule.AccountCapsule; @@ -141,6 +142,8 @@ public void doMaintenance() { witnessCapsule.setIsJobs(true); consensusDelegate.saveWitness(witnessCapsule); }); + + SRMetrics.recordSrSetChange(currentWits, newWits); } logger.info("Update witness success. \nbefore: {} \nafter: {}", 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/build.gradle b/framework/build.gradle index d884b6a7c49..1aa266da3cd 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -42,8 +42,8 @@ dependencies { implementation group: 'io.dropwizard.metrics', name: 'metrics-core', version: '3.1.2' implementation group: 'com.github.davidb', name: 'metrics-influxdb', version: '0.8.2' // http - implementation 'org.eclipse.jetty:jetty-server:9.4.57.v20241219' - implementation 'org.eclipse.jetty:jetty-servlet:9.4.57.v20241219' + implementation 'org.eclipse.jetty:jetty-server:9.4.58.v20250814' + implementation 'org.eclipse.jetty:jetty-servlet:9.4.58.v20250814' implementation 'com.alibaba:fastjson:1.2.83' // end http @@ -53,7 +53,7 @@ dependencies { // https://mvnrepository.com/artifact/javax.portlet/portlet-api compileOnly group: 'javax.portlet', name: 'portlet-api', version: '3.0.1' - implementation (group: 'org.pf4j', name: 'pf4j', version: '3.10.0') { + implementation (group: 'org.pf4j', name: 'pf4j', version: '3.14.1') { exclude group: "org.slf4j", module: "slf4j-api" } 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/common/logsfilter/ContractEventParser.java b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java index 48181cb1255..ceefa9a8cae 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java +++ b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParser.java @@ -1,8 +1,7 @@ package org.tron.common.logsfilter; -import static org.tron.common.math.Maths.min; - import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; @@ -38,9 +37,14 @@ public static String parseDataBytes(byte[] data, String typeStr, int index) { byte[] lengthBytes = subBytes(data, start, DATAWORD_UNIT_SIZE); // this length is byte count. no need X 32 int length = intValueExact(lengthBytes); + if (length < 0) { + throw new OutputLengthException("data length:" + length); + } byte[] realBytes = length > 0 ? subBytes(data, start + DATAWORD_UNIT_SIZE, length) : new byte[0]; - return type == Type.STRING ? new String(realBytes) : Hex.toHexString(realBytes); + return type == Type.STRING + ? new String(realBytes, StandardCharsets.UTF_8) + : Hex.toHexString(realBytes); } } catch (OutputLengthException | ArithmeticException e) { logger.debug("parseDataBytes ", e); @@ -74,11 +78,15 @@ protected static Integer intValueExact(byte[] data) { } protected static byte[] subBytes(byte[] src, int start, int length) { - if (ArrayUtils.isEmpty(src) || start >= src.length || length < 0) { - throw new OutputLengthException("data start:" + start + ", length:" + length); + if (ArrayUtils.isEmpty(src)) { + throw new OutputLengthException("source data is empty"); + } + if (start < 0 || start >= src.length || length < 0 || length > src.length - start) { + throw new OutputLengthException( + "data start:" + start + ", length:" + length + ", src.length:" + src.length); } byte[] dst = new byte[length]; - System.arraycopy(src, start, dst, 0, min(length, src.length - start, true)); + System.arraycopy(src, start, dst, 0, length); return dst; } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..ce3c3ac68f1 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -32,11 +32,6 @@ import static org.tron.core.config.Parameter.DatabaseConstants.PROPOSAL_COUNT_LIMIT_MAX; import static org.tron.core.config.Parameter.DatabaseConstants.WITNESS_COUNT_LIMIT_MAX; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseEnergyFee; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; import static org.tron.core.vm.utils.FreezeV2Util.getV2EnergyUsage; import static org.tron.core.vm.utils.FreezeV2Util.getV2NetUsage; import static org.tron.protos.contract.Common.ResourceCode; @@ -193,7 +188,6 @@ import org.tron.core.exception.VMIllegalException; import org.tron.core.exception.ValidateSignatureException; import org.tron.core.exception.ZksnarkException; -import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.TronNetService; import org.tron.core.net.message.adv.TransactionMessage; @@ -247,7 +241,6 @@ import org.tron.protos.contract.BalanceContract.BlockBalanceTrace; import org.tron.protos.contract.BalanceContract.TransferContract; import org.tron.protos.contract.Common; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; import org.tron.protos.contract.ShieldContract.OutputPoint; import org.tron.protos.contract.ShieldContract.OutputPointInfo; @@ -263,7 +256,9 @@ @Component public class Wallet { - private static final String SHIELDED_ID_NOT_ALLOWED = "ShieldedTransactionApi is not allowed"; + private static final String SHIELDED_ID_NOT_ALLOWED = + "Shielded transaction API is disabled; " + + "set node.allowShieldedTransactionApi=true to enable."; private static final String PAYMENT_ADDRESS_FORMAT_WRONG = "paymentAddress format is wrong"; private static final String SHIELDED_TRANSACTION_SCAN_RANGE = "request requires start_block_index >= 0 && end_block_index > " @@ -575,41 +570,41 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(true).setCode(response_code.SUCCESS).build(); } } catch (ValidateSignatureException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.SIGERROR) .setMessage(ByteString.copyFromUtf8("Validate signature error: " + e.getMessage())) .build(); } catch (ContractValidateException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) .setMessage(ByteString.copyFromUtf8(CONTRACT_VALIDATE_ERROR + e.getMessage())) .build(); } catch (ContractExeException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) .setMessage(ByteString.copyFromUtf8("Contract execute error : " + e.getMessage())) .build(); } catch (AccountResourceInsufficientException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.BANDWITH_ERROR) .setMessage(ByteString.copyFromUtf8("Account resource insufficient error.")) .build(); } catch (DupTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8("Dup transaction.")) .build(); } catch (TaposException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TAPOS_ERROR) .setMessage(ByteString.copyFromUtf8("Tapos check error.")) .build(); } catch (TooBigTransactionException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); } catch (TransactionExpirationException e) { - logger.warn(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); @@ -711,6 +706,10 @@ public long getSolidBlockNum() { return chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); } + public long getHeadBlockNum() { + return chainBaseManager.getHeadBlockNum(); + } + public BlockCapsule getBlockCapsuleByNum(long blockNum) { try { return chainBaseManager.getBlockByNum(blockNum); @@ -733,37 +732,6 @@ public long getTransactionCountByBlockNum(long blockNum) { return count; } - public Block getByJsonBlockId(String id) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(id)) { - return getBlockByNum(0); - } else if (LATEST_STR.equalsIgnoreCase(id)) { - return getNowBlock(); - } else if (FINALIZED_STR.equalsIgnoreCase(id)) { - return getSolidBlock(); - } else if (PENDING_STR.equalsIgnoreCase(id)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); - } else { - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(id).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid block number"); - } - - return getBlockByNum(blockNumber); - } - } - - public List getTransactionsByJsonBlockId(String id) - throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(id)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); - } else { - Block block = getByJsonBlockId(id); - return block != null ? block.getTransactionsList() : null; - } - } - public WitnessList getWitnessList() { WitnessList.Builder builder = WitnessList.newBuilder(); List witnessCapsuleList = chainBaseManager.getWitnessStore().getAllWitnesses(); @@ -780,7 +748,7 @@ public WitnessList getPaginatedNowWitnessList(long offset, long limit) throws if (limit > WITNESS_COUNT_LIMIT_MAX) { limit = WITNESS_COUNT_LIMIT_MAX; } - + /* In the maintenance period, the VoteStores will be cleared. To avoid the race condition of VoteStores deleted but Witness vote counts not updated, @@ -1502,8 +1470,8 @@ public Protocol.ChainParameters getChainParameters() { builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getAllowTvmSelfdestructRestriction") .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmSelfdestructRestriction()) - .build()); - + .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getProposalExpireTime") .setValue(dbManager.getDynamicPropertiesStore().getProposalExpireTime()) @@ -1514,6 +1482,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowHardenResourceCalculation") + .setValue(dbManager.getDynamicPropertiesStore().getAllowHardenResourceCalculation()) + .build()); + return builder.build(); } @@ -2207,23 +2180,6 @@ public IncrementalMerkleVoucherInfo getMerkleTreeVoucherInfo(OutputPointInfo req return result.build(); } - public IncrementalMerkleTree getMerkleTreeOfBlock(long blockNum) throws ZksnarkException { - checkAllowShieldedTransactionApi(); - if (blockNum < 0) { - return null; - } - - try { - if (chainBaseManager.getMerkleTreeIndexStore().has(ByteArray.fromLong(blockNum))) { - return IncrementalMerkleTree - .parseFrom(chainBaseManager.getMerkleTreeIndexStore().get(blockNum)); - } - } catch (Exception ex) { - logger.error("GetMerkleTreeOfBlock failed, blockNum:{}", blockNum, ex); - } - - return null; - } public long getShieldedTransactionFee() { return chainBaseManager.getDynamicPropertiesStore().getShieldedTransactionFee(); @@ -2320,58 +2276,58 @@ public TransactionCapsule createShieldedTransaction(PrivateParameters request) checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } - - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } - - // input - if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } + + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } + + // input + if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(expsk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(expsk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.build(); + return builder.build(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction except, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction except, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( @@ -2422,59 +2378,60 @@ public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } - // input - if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + // input + if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress( + note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(ak, + nsk, + ovk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(ak, - nsk, - ovk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.buildWithoutAsk(); + return builder.buildWithoutAsk(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } catch (ZksnarkException e) { - logger.error("createShieldedTransaction exception, error is " + e.toString()); - throw new ZksnarkException(e.toString()); + logger.error("createShieldedTransaction exception, error is {}", e.toString()); + throw e; } - return transactionCapsule; - } private void shieldedOutput(List shieldedReceives, @@ -2492,7 +2449,6 @@ private void shieldedOutput(List shieldedReceives, } } - public ShieldedAddressInfo getNewShieldedAddress() throws BadItemException, ZksnarkException { checkAllowShieldedTransactionApi(); @@ -2958,13 +2914,6 @@ public MarketOrderList getMarketOrderListByPair(byte[] sellTokenId, byte[] buyTo return builder.build(); } - public Transaction deployContract(TransactionCapsule trxCap) { - - // do nothing, so can add some useful function later - // trxCap contract para cacheUnpackValue has value - - return trxCap.getInstance(); - } public Transaction triggerContract(TriggerSmartContract triggerSmartContract, @@ -3658,77 +3607,80 @@ public ShieldedTRC20Parameters createShieldedContractParameters( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), dbManager.getDynamicPropertiesStore().disableJavaLangMath())); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } - builder.setTransparentFromAmount(fromAmount); - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + builder.setTransparentFromAmount(fromAmount); + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20Input(builder, spendNote, expsk); - } + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20Input(builder, spendNote, expsk); + } - for (ReceiveNote receiveNote : shieldedReceives) { - buildShieldedTRC20Output(builder, receiveNote, ovk); - } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No valid transparent TRC-20 output address"); - } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No valid transparent TRC-20 output address"); + } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20Input(builder, spendNote, expsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20Input(builder, spendNote, expsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(true); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } - - return builder.build(true); } private void buildShieldedTRC20InputWithAK( @@ -3791,65 +3743,69 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), chainBaseManager.getDynamicPropertiesStore().disableJavaLangMath()); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("Unbalanced burn!", e); } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - builder.setTransparentFromAmount(fromAmount); - ReceiveNote receiveNote = shieldedReceives.get(0); - buildShieldedTRC20Output(builder, receiveNote, ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - } - for (ReceiveNote receiveNote : shieldedReceives) { + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + builder.setTransparentFromAmount(fromAmount); + ReceiveNote receiveNote = shieldedReceives.get(0); buildShieldedTRC20Output(builder, receiveNote, ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No transparent TRC-20 output address"); + } + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No transparent TRC-20 output address"); - } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(false); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow", e); } - return builder.build(false); } private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddress, @@ -4596,4 +4552,3 @@ public PricesResponseMessage getMemoFeePrices() { return null; } } - diff --git a/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java b/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java index f497b9a85d8..4ee8d58483c 100644 --- a/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java +++ b/framework/src/main/java/org/tron/core/config/TronLogShutdownHook.java @@ -2,7 +2,6 @@ import ch.qos.logback.core.hook.ShutdownHookBase; import ch.qos.logback.core.util.Duration; -import org.tron.program.FullNode; /** * @author kiven @@ -16,11 +15,24 @@ public class TronLogShutdownHook extends ShutdownHookBase { private static final Duration CHECK_SHUTDOWN_DELAY = Duration.buildByMilliseconds(100); /** - * The check times before shutdown. default is 60000/100 = 600 times. + * Maximum time to wait for a graceful application shutdown before forcing + * a log flush. Each pool managed by ExecutorServiceManager.shutdownAndAwait- + * Termination() can take up to 120 s in the worst case (60 s await + + * shutdownNow + 60 s await). 180 s is therefore not a hard upper bound, but + * a pragmatic headroom that assumes the many pools in the node shut down + * largely in parallel; in pathological cases trailing shutdown logs may + * still be truncated. In practice 180 s of shutdown output is also enough + * to diagnose most stalls — if a pool is still alive past that window the + * earlier logs already carry the stack/trace context needed to locate the + * offender, so truncating the tail is an acceptable trade-off against + * holding JVM exit open indefinitely. */ - private final long check_times = 60 * 1000 / CHECK_SHUTDOWN_DELAY.getMilliseconds(); + private static final long MAX_WAIT_MS = 3 * 60 * 1000; - // if true, shutdown hook will be executed , for example, 'java -jar FullNode.jar -[v|h]'. + private static final long CHECK_TIMES = + MAX_WAIT_MS / CHECK_SHUTDOWN_DELAY.getMilliseconds(); + + // if true, shutdown hook will be executed, for example, 'java -jar FullNode.jar -[v|h]'. public static volatile boolean shutDown = true; public TronLogShutdownHook() { @@ -29,16 +41,19 @@ public TronLogShutdownHook() { @Override public void run() { try { - for (int i = 0; i < check_times; i++) { + for (long i = 0; i < CHECK_TIMES; i++) { if (shutDown) { break; } - addInfo("Sleeping for " + CHECK_SHUTDOWN_DELAY); + if (i % 100 == 0) { + long elapsedSeconds = i * CHECK_SHUTDOWN_DELAY.getMilliseconds() / 1000; + addInfo("Waiting for application shutdown... elapsed=" + elapsedSeconds + "s"); + } Thread.sleep(CHECK_SHUTDOWN_DELAY.getMilliseconds()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - addInfo("TronLogShutdownHook run error :" + e.getMessage()); + addInfo("TronLogShutdownHook interrupted: " + e.getMessage()); } super.stop(); } 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..e4966bd2c76 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,610 @@ 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.maxBlockInvPerSecond = nc.getMaxBlockInvPerSecond(); + 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 +958,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 +984,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 +1030,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 +1103,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 +1140,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 +1154,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/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index 1bec0c2bda3..54e0c6fa362 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -396,6 +396,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue()); break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + manager.getDynamicPropertiesStore() + .saveAllowHardenResourceCalculation(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index cd1a61c01fe..6ccd024091d 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1270,6 +1270,11 @@ public void pushBlock(final BlockCapsule block) synchronized (this) { Metrics.histogramObserve(blockedTimer.get()); blockedTimer.remove(); + if (Metrics.enabled()) { + Metrics.histogramObserve(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, + block.getTransactions().size(), + StringUtil.encode58Check(block.getWitnessAddress().toByteArray())); + } long headerNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); if (block.getNum() <= headerNumber && khaosDb.containBlockInMiniStore(block.getBlockId())) { logger.info("Block {} is already exist.", block.getBlockId().getString()); diff --git a/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java b/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java index 384f1d8add1..f39cf66a8ad 100644 --- a/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java +++ b/framework/src/main/java/org/tron/core/metrics/blockchain/BlockChainMetricManager.java @@ -164,9 +164,10 @@ public void applyBlock(BlockCapsule block) { } //TPS - if (block.getTransactions().size() > 0) { - MetricsUtil.meterMark(MetricsKey.BLOCKCHAIN_TPS, block.getTransactions().size()); - Metrics.counterInc(MetricKeys.Counter.TXS, block.getTransactions().size(), + int txCount = block.getTransactions().size(); + if (txCount > 0) { + MetricsUtil.meterMark(MetricsKey.BLOCKCHAIN_TPS, txCount); + Metrics.counterInc(MetricKeys.Counter.TXS, txCount, MetricLabels.Counter.TXS_SUCCESS, MetricLabels.Counter.TXS_SUCCESS); } } diff --git a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java index 9cfa5058e8c..b9173b95cde 100644 --- a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java +++ b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java @@ -36,6 +36,7 @@ import org.tron.core.net.service.effective.EffectiveCheckService; import org.tron.core.net.service.handshake.HandshakeService; import org.tron.core.net.service.keepalive.KeepAliveService; +import org.tron.core.net.service.statistics.MessageStatistics; import org.tron.p2p.P2pEventHandler; import org.tron.p2p.connection.Channel; import org.tron.protos.Protocol; @@ -91,6 +92,7 @@ public class P2pEventHandlerImpl extends P2pEventHandler { private byte MESSAGE_MAX_TYPE = 127; private int maxCountIn10s = Args.getInstance().getMaxTps() * 10; + private int maxBlockInvIn10s = Args.getInstance().getMaxBlockInvPerSecond() * 10; public P2pEventHandlerImpl() { Set set = new HashSet<>(); @@ -149,19 +151,8 @@ private void processMessage(PeerConnection peer, byte[] data) { msg = TronMessageFactory.create(data); type = msg.getType(); - if (INVENTORY.equals(type)) { - InventoryMessage message = (InventoryMessage) msg; - Protocol.Inventory.InventoryType inventoryType = message.getInventoryType(); - int count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement - .getCount(10); - if (inventoryType.equals(Protocol.Inventory.InventoryType.TRX) && count > maxCountIn10s) { - logger.warn("Drop inventory from Peer {}, cur:{}, max:{}", - peer.getInetAddress(), count, maxCountIn10s); - if (Args.getInstance().isOpenPrintLog()) { - logger.warn("[overload]Drop tx list is: {}", ((InventoryMessage) msg).getHashList()); - } - return; - } + if (INVENTORY.equals(type) && !checkInvRateLimit(peer, (InventoryMessage) msg)) { + return; } peer.getPeerStatistics().messageStatistics.addTcpInMessage(msg); @@ -224,6 +215,32 @@ private void processMessage(PeerConnection peer, byte[] data) { } } + private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) { + InventoryType invType = msg.getInventoryType(); + int currentSize = msg.getInventory().getIdsCount(); + MessageStatistics stats = peer.getPeerStatistics().messageStatistics; + + if (invType == InventoryType.TRX) { + int count = stats.tronInTrxInventoryElement.getCount(10); + if (count + currentSize > maxCountIn10s) { + logger.warn("Drop TRX inv from {}, window:{}, cur:{}, max:{}", + peer.getInetAddress(), count, currentSize, maxCountIn10s); + if (Args.getInstance().isOpenPrintLog()) { + logger.warn("[overload] Drop tx list: {}", msg.getHashList()); + } + return false; + } + } else if (invType == InventoryType.BLOCK) { + int count = stats.tronInBlockInventoryElement.getCount(10); + if (count + currentSize > maxBlockInvIn10s) { + logger.warn("Drop BLOCK inv from {}, window:{}, cur:{}, max:{}", + peer.getInetAddress(), count, currentSize, maxBlockInvIn10s); + return false; + } + } + return true; + } + private void updateLastInteractiveTime(PeerConnection peer, TronMessage msg) { MessageTypes type = msg.getType(); diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 63e7ba03fc7..bc50b79a36f 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -418,7 +418,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.error("Solidity NonUniqueObjectException: {}", e.getMessage()); + logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); 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/GetBurnTrxServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java index e574affff6b..ea066a6e98c 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java @@ -19,7 +19,10 @@ public class GetBurnTrxServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getDynamicPropertiesStore().getBurnTrxAmount(); - response.getWriter().println("{\"burnTrxAmount\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"burnTrxAmount\": \"" + value + "\"}" + : "{\"burnTrxAmount\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java index 7e1a5f71841..9788c926586 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java @@ -19,7 +19,10 @@ public class GetPendingSizeServlet extends RateLimiterServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { try { long value = manager.getPendingSize(); - response.getWriter().println("{\"pendingSize\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"pendingSize\": \"" + value + "\"}" + : "{\"pendingSize\": " + value + "}"; + response.getWriter().println(out); } catch (Exception e) { logger.error("", e); try { diff --git a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java index c4d97f46c57..61b88d1160f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java @@ -24,7 +24,10 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { if (address != null) { value = manager.getMortgageService().queryReward(address); } - response.getWriter().println("{\"reward\": " + value + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"reward\": \"" + value + "\"}" + : "{\"reward\": " + value + "}"; + response.getWriter().println(out); } catch (DecoderException | IllegalArgumentException e) { try { response.getWriter() diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java index e096df507d7..81c1ece73fb 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java @@ -40,6 +40,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) private void fillResponse(long num, HttpServletResponse response) throws IOException { long count = wallet.getTransactionCountByBlockNum(num); - response.getWriter().println("{\"count\": " + count + "}"); + String out = JsonFormat.isInt64AsString() + ? "{\"count\": \"" + count + "\"}" + : "{\"count\": " + count + "}"; + response.getWriter().println(out); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 96dedb1e20c..8a8c66fb371 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -90,6 +90,41 @@ public class JsonFormat { BalanceContract.TransactionBalanceTrace.class ); + /** + * Thread-local flag controlling whether int64/uint64 fields are serialized as JSON strings. + * Set via {@link #setInt64AsString(boolean)} early in request handling and cleared via + * {@link #clearInt64AsString()} in a finally block. Centralized in + * {@code RateLimiterServlet.service} for GET requests. Does not support nested scopes. + */ + private static final ThreadLocal INT64_AS_STRING = + ThreadLocal.withInitial(() -> false); + + /** + * Set whether int64/uint64 protobuf fields are serialized as quoted JSON strings to avoid + * precision loss in clients whose native number type cannot safely represent integers above + * 2^53 - 1 (e.g. JavaScript). Must be paired with {@link #clearInt64AsString()} in a + * finally block. + */ + public static void setInt64AsString(boolean enabled) { + INT64_AS_STRING.set(enabled); + } + + /** + * Clear the int64-as-string thread-local. Always call from a finally block to avoid + * polluting subsequent requests on the same (reused) thread. + */ + public static void clearInt64AsString() { + INT64_AS_STRING.remove(); + } + + /** + * Whether the current thread is in int64-as-string mode. Used by servlets that build + * JSON literals manually (i.e. do not go through {@link #printToString}). + */ + public static boolean isInt64AsString() { + return INT64_AS_STRING.get(); + } + /** * Outputs a textual representation of the Protocol Message supplied into the parameter output. * (This representation is the new version of the classic "ProtocolPrinter" output from the @@ -340,11 +375,8 @@ private static void printFieldValue(FieldDescriptor field, Object value, throws IOException { switch (field.getType()) { case INT32: - case INT64: case SINT32: - case SINT64: case SFIXED32: - case SFIXED64: case FLOAT: case DOUBLE: case BOOL: @@ -352,6 +384,18 @@ private static void printFieldValue(FieldDescriptor field, Object value, generator.print(value.toString()); break; + case INT64: + case SINT64: + case SFIXED64: + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(value.toString()); + generator.print("\""); + } else { + generator.print(value.toString()); + } + break; + case UINT32: case FIXED32: generator.print(unsignedToString((Integer) value)); @@ -359,7 +403,13 @@ private static void printFieldValue(FieldDescriptor field, Object value, case UINT64: case FIXED64: - generator.print(unsignedToString((Long) value)); + if (INT64_AS_STRING.get()) { + generator.print("\""); + generator.print(unsignedToString((Long) value)); + generator.print("\""); + } else { + generator.print(unsignedToString((Long) value)); + } break; case STRING: diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index 7a66aed34f6..f173fbcaa82 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -3,7 +3,11 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,56 +35,66 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + static final Map> ALLOWED_ADAPTERS; + static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + List> adapters = Arrays.asList( + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class); + Map> m = new HashMap<>(); + for (Class c : adapters) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @PostConstruct private void addRateContainer() { - RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() - .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } + RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() + .getRateLimiterInitialization().getHttpMap().get(name); + + String cName; + String params; + if (item == null) { + cName = DEFAULT_ADAPTER_NAME; + params = QpsStrategy.DEFAULT_QPS_PARAM; + } else { + cName = item.getStrategy(); + params = item.getParams(); } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); - } + + try { + container.add(KEY_PREFIX_HTTP, name, buildAdapter(cName, params, name)); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); + } + } + + static IRateLimiter buildAdapter(String cName, String params, String name) { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw rateLimiterInitError(cName, params, name, + new IllegalArgumentException("unknown rate limiter adapter; allowed=" + + ALLOWED_ADAPTERS.keySet())); + } + try { + return c.getConstructor(String.class).newInstance(params); + } catch (Exception e) { + throw rateLimiterInitError(cName, params, name, e); } } - private void throwTronError(String strategy, String params, String servlet, Exception e) { - throw new TronError("failure to add the rate limiter strategy. servlet = " + servlet + private static TronError rateLimiterInitError(String strategy, String params, String servlet, + Exception e) { + return new TronError("failure to add the rate limiter strategy. servlet = " + servlet + ", strategy name = " + strategy + ", params = \"" + params + "\".", e, TronError.ErrCode.RATE_LIMITER_INIT); } @@ -102,6 +116,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) String contextPath = req.getContextPath(); String url = Strings.isNullOrEmpty(req.getServletPath()) ? MetricLabels.UNDEFINED : contextPath + req.getServletPath(); + // int64_as_string is honored only on GET requests (URL query). POST is intentionally + // unsupported because reading the body here would consume request.getReader() and + // break downstream servlets that read it themselves. + if ("GET".equalsIgnoreCase(req.getMethod())) { + JsonFormat.setInt64AsString(Util.getInt64AsString(req)); + } try { resp.setContentType("application/json; charset=utf-8"); @@ -119,6 +139,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) } catch (Exception unexpected) { logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected); } finally { + // CRITICAL: this clear pairs with the setInt64AsString call above. Removing it + // will leak int64_as_string state across requests on reused Tomcat threads, + // producing intermittent quoted/unquoted output that is very hard to debug. + JsonFormat.clearInt64AsString(); if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) { ((IPreemptibleRateLimiter) rateLimiter).release(); } 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..8bf0218c166 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 @@ -66,6 +66,7 @@ public class Util { public static final String PERMISSION_ID = "Permission_id"; public static final String VISIBLE = "visible"; + public static final String INT64_AS_STRING_PARAM = "int64_as_string"; public static final String TRANSACTION = "transaction"; public static final String TRANSACTION_EXTENSION = "transactionExtension"; public static final String VALUE = "value"; @@ -95,7 +96,7 @@ public static String printErrorMsg(Exception e) { public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(list, selfType)); + JSONObject jsonObject = new JSONObject(); JSONArray jsonArray = new JSONArray(); blocks.stream().forEach(block -> jsonArray.add(printBlockToJSON(block, selfType))); jsonObject.put("block", jsonArray); @@ -110,8 +111,10 @@ public static String printBlock(Block block, boolean selfType) { public static JSONObject printBlockToJSON(Block block, boolean selfType) { BlockCapsule blockCapsule = new BlockCapsule(block); String blockID = ByteArray.toHexString(blockCapsule.getBlockId().getBytes()); - JSONObject jsonObject = JSONObject.parseObject(JsonFormat.printToString(block, selfType)); + JSONObject jsonObject = new JSONObject(); jsonObject.put("blockID", blockID); + jsonObject.put("block_header", + JSONObject.parseObject(JsonFormat.printToString(block.getBlockHeader(), selfType))); if (!blockCapsule.getTransactions().isEmpty()) { jsonObject.put("transactions", printTransactionListToJSON(blockCapsule.getTransactions(), selfType)); @@ -327,10 +330,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()); } } @@ -346,6 +351,21 @@ public static boolean existVisible(final HttpServletRequest request) { return Objects.nonNull(request.getParameter(VISIBLE)); } + /** + * Read int64_as_string from URL query parameter. Mirrors + * {@link #getVisible(HttpServletRequest)}. The flag is honored only on GET requests + * (read by {@link RateLimiterServlet#service}); POST requests do not support it + * because that would require caching the request body to allow re-reading by + * downstream servlets. + */ + public static boolean getInt64AsString(final HttpServletRequest request) { + boolean int64AsString = false; + if (StringUtil.isNotBlank(request.getParameter(INT64_AS_STRING_PARAM))) { + int64AsString = Boolean.valueOf(request.getParameter(INT64_AS_STRING_PARAM)); + } + return int64AsString; + } + public static boolean getVisiblePost(final String input) { boolean visible = false; if (StringUtil.isNotBlank(input)) { 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/services/jsonrpc/JsonRpcApiUtil.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java index 4a60f14b534..104b72a66e8 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcApiUtil.java @@ -1,15 +1,10 @@ package org.tron.core.services.jsonrpc; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.EARLIEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.FINALIZED_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.LATEST_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.PENDING_STR; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; - import com.google.common.base.Throwables; import com.google.common.primitives.Longs; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; @@ -57,6 +52,15 @@ @Slf4j(topic = "API") public class JsonRpcApiUtil { + public static final String EARLIEST_STR = "earliest"; + public static final String PENDING_STR = "pending"; + public static final String LATEST_STR = "latest"; + public static final String FINALIZED_STR = "finalized"; + public static final String SAFE_STR = "safe"; + public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; + public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; + public static final String BLOCK_NUM_ERROR = "invalid block number"; + public static byte[] convertToTronAddress(byte[] address) { byte[] newAddress = new byte[21]; byte[] temp = new byte[] {Wallet.getAddressPreFixByte()}; @@ -439,6 +443,50 @@ public static boolean paramQuantityIsNull(String quantity) { return StringUtils.isEmpty(quantity) || quantity.equals("0x0"); } + /** + * Validation mode for {@link #requireValidHex}. + */ + public enum HexMode { + /** + * Execution-apis BYTES schema: requires {@code 0x} prefix and + * even total length; {@code ""} is accepted as empty bytes per + * geth's {@code hexutil.Bytes.UnmarshalText}. + */ + STRICT, + /** + * {@link ByteArray#fromHexString}'s lenient parsing: accepts bare + * hex and odd-length input. Kept for backward compatibility. + */ + LENIENT + } + + /** + * Throws if {@code value} is not parseable hex under the given + * {@code mode}. {@code null} is treated as absent and returns + * silently. {@code fieldName} is used only in error messages. + */ + public static void requireValidHex(String fieldName, String value, HexMode mode) + throws JsonRpcInvalidParamsException { + if (value == null) { + return; + } + if (mode == HexMode.STRICT) { + if (value.isEmpty()) { + return; + } + if (!value.startsWith("0x") || value.length() % 2 != 0) { + throw new JsonRpcInvalidParamsException( + "invalid hex string for \"" + fieldName + "\""); + } + } + try { + ByteArray.fromHexString(value); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException( + "invalid hex string for \"" + fieldName + "\""); + } + } + public static long parseQuantityValue(String value) throws JsonRpcInvalidParamsException { long callValue = 0L; @@ -515,20 +563,87 @@ public static long parseEnergyFee(long timestamp, String energyPriceHistory) { return -1; } - public static long getByJsonBlockId(String blockNumOrTag, Wallet wallet) + public static boolean isBlockTag(String tag) { + return LATEST_STR.equalsIgnoreCase(tag) + || EARLIEST_STR.equalsIgnoreCase(tag) + || FINALIZED_STR.equalsIgnoreCase(tag) + || PENDING_STR.equalsIgnoreCase(tag) + || SAFE_STR.equalsIgnoreCase(tag); + } + + /** + * Parse a block tag (latest, earliest, finalized) to block number. + * + *

Note: for "latest", the returned block number may not yet be available in + * blockStore or blockIndexStore due to write ordering. Callers that need the + * actual block must handle the not-found case.

+ */ + public static long parseBlockTag(String tag, Wallet wallet) throws JsonRpcInvalidParamsException { - if (PENDING_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + if (LATEST_STR.equalsIgnoreCase(tag)) { + return wallet.getHeadBlockNum(); } - if (StringUtils.isEmpty(blockNumOrTag) || LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - return -1; - } else if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag)) { + if (EARLIEST_STR.equalsIgnoreCase(tag)) { return 0; - } else if (FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { + } + if (FINALIZED_STR.equalsIgnoreCase(tag)) { return wallet.getSolidBlockNum(); - } else { - return ByteArray.jsonHexToLong(blockNumOrTag); } + if (PENDING_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_PENDING_SUPPORT_ERROR); + } + if (SAFE_STR.equalsIgnoreCase(tag)) { + throw new JsonRpcInvalidParamsException(TAG_SAFE_SUPPORT_ERROR); + } + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + + /** + * Max allowed length for a JSON-RPC block number hex/decimal input. + * API-level DoS guard: rejects pathological inputs before BigInteger parsing, + * whose cost grows quadratically with length. Covers hex (0x + 64 chars for + * uint256) and decimal (78 chars for uint256) representations with headroom. + */ + private static final int MAX_BLOCK_NUM_HEX_LEN = 100; + + /** + * Parse a JSON-RPC block number (hex "0x..." or decimal) into a long, + * enforcing the {@link #MAX_BLOCK_NUM_HEX_LEN} length limit, rejecting + * negative values, and rejecting values that overflow a signed 64-bit + * block number. + */ + public static long parseBlockNumber(String blockNum) + throws JsonRpcInvalidParamsException { + if (blockNum == null || blockNum.length() > MAX_BLOCK_NUM_HEX_LEN) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + BigInteger value; + try { + value = ByteArray.hexToBigInteger(blockNum); + } catch (Exception e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + if (value.signum() < 0) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + try { + return value.longValueExact(); + } catch (ArithmeticException e) { + throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); + } + } + + /** + * Parse a block tag or hex number. Uses strict jsonHexToLong (requires 0x prefix) for hex. + * Callers needing flexible hex parsing (0x -> hex, bare number -> decimal) should use + * isBlockTag/parseBlockTag and handle hex separately with hexToBigInteger. + */ + public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) + throws JsonRpcInvalidParamsException { + if (isBlockTag(blockNumOrTag)) { + return parseBlockTag(blockNumOrTag, wallet); + } + return ByteArray.jsonHexToLong(blockNumOrTag); } public static String generateFilterId() { diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java index 115df6ef9da..8e7c8615da4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpc.java @@ -461,10 +461,12 @@ class LogFilterElement { private final String[] topics; @Getter private final boolean removed; + @Getter + private final String blockTimestamp; public LogFilterElement(String blockHash, Long blockNum, String txId, Integer txIndex, String contractAddress, List topicList, String logData, int logIdx, - boolean removed) { + boolean removed, long blockTimestampMs) { logIndex = ByteArray.toJsonHex(logIdx); this.blockNumber = blockNum == null ? null : ByteArray.toJsonHex(blockNum); this.blockHash = blockHash == null ? null : ByteArray.toJsonHex(blockHash); @@ -477,6 +479,7 @@ public LogFilterElement(String blockHash, Long blockNum, String txId, Integer tx topics[i] = ByteArray.toJsonHex(topicList.get(i).getData()); } this.removed = removed; + this.blockTimestamp = ByteArray.toJsonHex(blockTimestampMs / 1000); } @Override @@ -500,12 +503,16 @@ public boolean equals(Object o) { if (!Objects.equals(logIndex, item.logIndex)) { return false; } - return removed == item.removed; + if (removed != item.removed) { + return false; + } + return Objects.equals(blockTimestamp, item.blockTimestamp); } @Override public int hashCode() { - return Objects.hash(blockHash, transactionHash, transactionIndex, logIndex, removed); + return Objects.hash(blockHash, transactionHash, transactionIndex, + logIndex, removed, blockTimestamp); } } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index de939bdfff4..663b39de290 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -3,11 +3,15 @@ import static org.tron.core.Wallet.CONTRACT_VALIDATE_ERROR; import static org.tron.core.services.http.Util.setTransactionExtraData; import static org.tron.core.services.http.Util.setTransactionPermissionId; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.BLOCK_NUM_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.FINALIZED_STR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.generateFilterId; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getEnergyUsageTotal; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTransactionIndex; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getTxID; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.triggerCallContract; import com.alibaba.fastjson.JSON; @@ -152,23 +156,19 @@ public enum RequestSource { public static final String HASH_REGEX = "(0x)?[a-zA-Z0-9]{64}$"; - public static final String EARLIEST_STR = "earliest"; - public static final String PENDING_STR = "pending"; - public static final String LATEST_STR = "latest"; - public static final String FINALIZED_STR = "finalized"; - public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; public static final String INVALID_BLOCK_RANGE = "invalid block range params"; private static final String JSON_ERROR = "invalid json request"; - private static final String BLOCK_NUM_ERROR = "invalid block number"; private static final String TAG_NOT_SUPPORT_ERROR = - "TAG [earliest | pending | finalized] not supported"; + "TAG [earliest | pending | finalized | safe] not supported"; private static final String QUANTITY_NOT_SUPPORT_ERROR = "QUANTITY not supported, just support TAG as latest"; private static final String NO_BLOCK_HEADER = "header not found"; private static final String NO_BLOCK_HEADER_BY_HASH = "header for hash not found"; private static final String ERROR_SELECTOR = "08c379a0"; // Function selector for Error(string) + private static final int REVERT_REASON_SELECTOR_LENGTH = 4; + private static final int MAX_REVERT_REASON_PAYLOAD_BYTES = 4096; /** * thread pool of query section bloom store */ @@ -308,12 +308,12 @@ public String ethGetBlockTransactionCountByHash(String blockHash) @Override public String ethGetBlockTransactionCountByNumber(String blockNumOrTag) throws JsonRpcInvalidParamsException { - List list = wallet.getTransactionsByJsonBlockId(blockNumOrTag); - if (list == null) { + Block block = getBlockByNumOrTag(blockNumOrTag); + if (block == null) { return null; } - long n = list.size(); + long n = block.getTransactionsCount(); return ByteArray.toJsonHex(n); } @@ -327,7 +327,7 @@ public BlockResult ethGetBlockByHash(String blockHash, Boolean fullTransactionOb @Override public BlockResult ethGetBlockByNumber(String blockNumOrTag, Boolean fullTransactionObjects) throws JsonRpcInvalidParamsException { - final Block b = wallet.getByJsonBlockId(blockNumOrTag); + final Block b = getBlockByNumOrTag(blockNumOrTag); return (b == null ? null : getBlockResult(b, fullTransactionObjects)); } @@ -345,11 +345,39 @@ private byte[] hashToByteArray(String hash) throws JsonRpcInvalidParamsException return bHash; } + /** + * Reject any block selector that is not "latest". + * Accepts "latest" silently; throws for other tags, numeric blocks, or invalid input. + */ + private void requireLatestBlockTag(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + return; + } + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); + } + parseBlockNumber(blockNumOrTag); + throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + } + private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsException { byte[] bHash = hashToByteArray(blockHash); return wallet.getBlockById(ByteString.copyFrom(bHash)); } + private Block getBlockByNumOrTag(String blockNumOrTag) throws JsonRpcInvalidParamsException { + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + // Return the head block directly from blockStore, bypassing blockIndexStore + // which may not yet be written when latestBlockHeaderNumber is already updated. + return wallet.getNowBlock(); + } + return wallet.getBlockByNum(JsonRpcApiUtil.parseBlockTag(blockNumOrTag, wallet)); + } + return wallet.getBlockByNum(parseBlockNumber(blockNumOrTag)); + } + private BlockResult getBlockResult(Block block, boolean fullTx) { if (block == null) { return null; @@ -393,30 +421,18 @@ public String getLatestBlockNum() { @Override public String getTrxBalance(String address, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(address); + requireLatestBlockTag(blockNumOrTag); - Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); - Account reply = wallet.getAccount(account); - long balance = 0; + byte[] addressData = addressCompatibleToByteArray(address); - if (reply != null) { - balance = reply.getBalance(); - } - return ByteArray.toJsonHex(balance); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); + Account reply = wallet.getAccount(account); + long balance = 0; - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + if (reply != null) { + balance = reply.getBalance(); } + return ByteArray.toJsonHex(balance); } private void callTriggerConstantContract(byte[] ownerAddressByte, byte[] contractAddressByte, @@ -469,6 +485,36 @@ private void estimateEnergy(byte[] ownerAddressByte, byte[] contractAddressByte, estimateBuilder.setResult(retBuilder); } + /** + * Decodes an Error(string) revert reason when possible. + * Returns ": reason" for a non-empty reason, otherwise "". + */ + static String tryDecodeRevertReason(byte[] resData) { + if (resData == null || resData.length <= REVERT_REASON_SELECTOR_LENGTH) { + return ""; + } + if (!Hex.toHexString(resData, 0, REVERT_REASON_SELECTOR_LENGTH).equals(ERROR_SELECTOR)) { + return ""; + } + + int revertPayloadLength = resData.length - REVERT_REASON_SELECTOR_LENGTH; + if (revertPayloadLength > MAX_REVERT_REASON_PAYLOAD_BYTES) { + logger.debug("skip parsing oversized revert reason payload: {} bytes", revertPayloadLength); + return ""; + } + + try { + String reason = ContractEventParser.parseDataBytes( + Arrays.copyOfRange(resData, REVERT_REASON_SELECTOR_LENGTH, + resData.length), + "string", 0); + return reason.isEmpty() ? "" : ": " + reason; + } catch (RuntimeException e) { + logger.debug("parse revert reason failed", e); + return ""; + } + } + /** * @param data Hash of the method signature and encoded parameters. for example: * getMethodSign(methodName(uint256,uint256)) || data1 || data2 @@ -512,14 +558,8 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va } result = ByteArray.toJsonHex(listBytes); } else { - String errMsg = retBuilder.getMessage().toStringUtf8(); byte[] resData = trxExtBuilder.getConstantResult(0).toByteArray(); - if (resData.length > 4 && Hex.toHexString(resData).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(resData, 4, resData.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(resData); if (resData.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(resData)); @@ -535,67 +575,42 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va @Override public String getStorageAt(String address, String storageIdx, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressByte = addressCompatibleToByteArray(address); - - // get contract from contractStore - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); - SmartContract smartContract = wallet.getContract(bytesMessage); - if (smartContract == null) { - return ByteArray.toJsonHex(new byte[32]); - } + requireLatestBlockTag(blockNumOrTag); - StorageRowStore store = manager.getStorageRowStore(); - Storage storage = new Storage(addressByte, store); - storage.setContractVersion(smartContract.getVersion()); - storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + byte[] addressByte = addressCompatibleToByteArray(address); - DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); - return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + // get contract from contractStore + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressByte)).build(); + SmartContract smartContract = wallet.getContract(bytesMessage); + if (smartContract == null) { + return ByteArray.toJsonHex(new byte[32]); } + + StorageRowStore store = manager.getStorageRowStore(); + Storage storage = new Storage(addressByte, store); + storage.setContractVersion(smartContract.getVersion()); + storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); + + DataWord value = storage.getValue(new DataWord(ByteArray.fromHexString(storageIdx))); + return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } @Override public String getABIOfSmartContract(String contractAddress, String blockNumOrTag) throws JsonRpcInvalidParamsException { - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(contractAddress); + requireLatestBlockTag(blockNumOrTag); - BytesMessage.Builder build = BytesMessage.newBuilder(); - BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); - SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + byte[] addressData = addressCompatibleToByteArray(contractAddress); - if (contractDataWrapper != null) { - return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); - } else { - return "0x"; - } + BytesMessage.Builder build = BytesMessage.newBuilder(); + BytesMessage bytesMessage = build.setValue(ByteString.copyFrom(addressData)).build(); + SmartContractDataWrapper contractDataWrapper = wallet.getContractInfo(bytesMessage); + if (contractDataWrapper != null) { + return ByteArray.toJsonHex(contractDataWrapper.getRuntimecode().toByteArray()); } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } - - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + return "0x"; } } @@ -647,7 +662,7 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept estimateEnergy(ownerAddress, contractAddress, args.parseValue(), - ByteArray.fromHexString(args.getData()), + ByteArray.fromHexString(args.resolveData()), trxExtBuilder, retBuilder, estimateBuilder); @@ -655,7 +670,7 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept callTriggerConstantContract(ownerAddress, contractAddress, args.parseValue(), - ByteArray.fromHexString(args.getData()), + ByteArray.fromHexString(args.resolveData()), trxExtBuilder, retBuilder); } @@ -677,15 +692,8 @@ public String estimateGas(CallArguments args) throws JsonRpcInvalidRequestExcept } if (trxExtBuilder.getTransaction().getRet(0).getRet().equals(code.FAILED)) { - String errMsg = retBuilder.getMessage().toStringUtf8(); - byte[] data = trxExtBuilder.getConstantResult(0).toByteArray(); - if (data.length > 4 && Hex.toHexString(data).startsWith(ERROR_SELECTOR)) { - String msg = ContractEventParser - .parseDataBytes(org.bouncycastle.util.Arrays.copyOfRange(data, 4, data.length), - "string", 0); - errMsg += ": " + msg; - } + String errMsg = retBuilder.getMessage().toStringUtf8() + tryDecodeRevertReason(data); if (data.length > 0) { throw new JsonRpcInternalException(errMsg, ByteArray.toJsonHex(data)); @@ -803,7 +811,7 @@ public TransactionResult getTransactionByBlockHashAndIndex(String blockHash, Str @Override public TransactionResult getTransactionByBlockNumberAndIndex(String blockNumOrTag, String index) throws JsonRpcInvalidParamsException { - Block block = wallet.getByJsonBlockId(blockNumOrTag); + Block block = getBlockByNumOrTag(blockNumOrTag); if (block == null) { return null; } @@ -894,7 +902,7 @@ public List getBlockReceipts(String blockNumOrHashOrTag) if (Pattern.matches(HASH_REGEX, blockNumOrHashOrTag)) { block = getBlockByJsonHash(blockNumOrHashOrTag); } else { - block = wallet.getByJsonBlockId(blockNumOrHashOrTag); + block = getBlockByNumOrTag(blockNumOrHashOrTag); } // block receipts not available: block is genesis, not produced yet, or pruned in light node @@ -971,12 +979,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - long blockNumber; - try { - blockNumber = ByteArray.hexToBigInteger(blockNumOrTag).longValue(); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER); @@ -1003,25 +1006,13 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidRequestException(JSON_ERROR); } - if (EARLIEST_STR.equalsIgnoreCase(blockNumOrTag) - || PENDING_STR.equalsIgnoreCase(blockNumOrTag) - || FINALIZED_STR.equalsIgnoreCase(blockNumOrTag)) { - throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); - } else if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { - byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); - byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); + requireLatestBlockTag(blockNumOrTag); - return call(addressData, contractAddressData, transactionCall.parseValue(), - ByteArray.fromHexString(transactionCall.getData())); - } else { - try { - ByteArray.hexToBigInteger(blockNumOrTag); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException(BLOCK_NUM_ERROR); - } + byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); + byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); - throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); - } + return call(addressData, contractAddressData, transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.resolveData())); } @Override @@ -1128,7 +1119,8 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, smartBuilder.setOriginAddress(ByteString.copyFrom(ownerAddress)); // bytecode + parameter - smartBuilder.setBytecode(ByteString.copyFrom(ByteArray.fromHexString(args.getData()))); + smartBuilder.setBytecode( + ByteString.copyFrom(ByteArray.fromHexString(args.resolveData()))); if (StringUtils.isNotEmpty(args.getName())) { smartBuilder.setName(args.getName()); @@ -1173,8 +1165,9 @@ private TransactionJson buildTriggerSmartContractTransaction(byte[] ownerAddress build.setOwnerAddress(ByteString.copyFrom(ownerAddress)) .setContractAddress(ByteString.copyFrom(contractAddress)); - if (StringUtils.isNotEmpty(args.getData())) { - build.setData(ByteString.copyFrom(ByteArray.fromHexString(args.getData()))); + String callData = args.resolveData(); + if (StringUtils.isNotEmpty(callData)) { + build.setData(ByteString.copyFrom(ByteArray.fromHexString(callData))); } else { build.setData(ByteString.copyFrom(new byte[0])); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java index 97a012b7f9a..0331ab3694a 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogFilterWrapper.java @@ -1,6 +1,7 @@ package org.tron.core.services.jsonrpc.filters; import static org.tron.common.math.StrictMathWrapper.min; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.LATEST_STR; import com.google.protobuf.ByteString; import lombok.Getter; @@ -50,39 +51,50 @@ public LogFilterWrapper(FilterRequest fr, long currentMaxBlockNum, Wallet wallet toBlockSrc = fromBlockSrc; } else { - // if fromBlock is empty but toBlock is not empty, - // then if toBlock < maxBlockNum, set fromBlock = toBlock - // then if toBlock >= maxBlockNum, set fromBlock = maxBlockNum - if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isNotEmpty(fr.getToBlock())) { - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (toBlockSrc == -1) { - toBlockSrc = Long.MAX_VALUE; - } - fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + // Normalize the request into one of four strategies based on parameter emptiness. + // Long.MAX_VALUE is an internal sentinel meaning "open upper bound"; it is never + // treated as a real block number by later query stages. + // Note: "latest" tag handling differs by strategy: + // - Strategy 2: toBlock="latest" -> Long.MAX_VALUE (track future blocks) + // - Strategy 3: fromBlock="latest" -> currentMaxBlockNum snapshot (bounded start) + // - Strategy 4: fromBlock="latest" -> currentMaxBlockNum; toBlock="latest" -> Long.MAX_VALUE - } else if (StringUtils.isNotEmpty(fr.getFromBlock()) - && StringUtils.isEmpty(fr.getToBlock())) { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - if (fromBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - } - toBlockSrc = Long.MAX_VALUE; + boolean fromEmpty = StringUtils.isEmpty(fr.getFromBlock()); + boolean toEmpty = StringUtils.isEmpty(fr.getToBlock()); - } else if (StringUtils.isEmpty(fr.getFromBlock()) && StringUtils.isEmpty(fr.getToBlock())) { + if (fromEmpty && toEmpty) { + // Strategy 1: Both parameters omitted. Start at the current head and track new blocks. fromBlockSrc = currentMaxBlockNum; toBlockSrc = Long.MAX_VALUE; - } else { - fromBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getFromBlock(), wallet); - toBlockSrc = JsonRpcApiUtil.getByJsonBlockId(fr.getToBlock(), wallet); - if (fromBlockSrc == -1 && toBlockSrc == -1) { - fromBlockSrc = currentMaxBlockNum; - toBlockSrc = Long.MAX_VALUE; - } else if (fromBlockSrc == -1 && toBlockSrc >= 0) { - fromBlockSrc = currentMaxBlockNum; - } else if (fromBlockSrc >= 0 && toBlockSrc == -1) { + } else if (fromEmpty) { + // Strategy 2: Only toBlock specified. + // If toBlock is "latest": track future blocks (fromBlock = currentMaxBlockNum, + // toBlock = MAX_VALUE). If concrete: bounded query with fromBlock = min(toBlock, + // currentMaxBlockNum). + if (LATEST_STR.equalsIgnoreCase(fr.getToBlock())) { toBlockSrc = Long.MAX_VALUE; + } else { + toBlockSrc = JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); } + fromBlockSrc = min(toBlockSrc, currentMaxBlockNum); + + } else if (toEmpty) { + // Strategy 3: Only fromBlock specified. Start at fromBlock and track new blocks. + // If fromBlock is "latest", use the snapshot (currentMaxBlockNum) as the starting point. + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = Long.MAX_VALUE; + + } else { + // Strategy 4: Both parameters specified. + // If fromBlock is "latest": use the snapshot (currentMaxBlockNum) as a fixed start point. + // If toBlock is "latest": use Long.MAX_VALUE to track future blocks. + // Otherwise: parse both as concrete block numbers + fromBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getFromBlock()) ? currentMaxBlockNum + : JsonRpcApiUtil.parseBlockNumber(fr.getFromBlock(), wallet); + toBlockSrc = LATEST_STR.equalsIgnoreCase(fr.getToBlock()) ? Long.MAX_VALUE + : JsonRpcApiUtil.parseBlockNumber(fr.getToBlock(), wallet); if (fromBlockSrc > toBlockSrc) { throw new JsonRpcInvalidParamsException("please verify: fromBlock <= toBlock"); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java index cf958d1e2cb..67d229b2948 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/filters/LogMatch.java @@ -66,7 +66,8 @@ public static List matchBlock(LogFilter logFilter, long blockN topicList, ByteArray.toHexString(log.getData().toByteArray()), logIndexInBlock, - removed + removed, + transactionInfo.getBlockTimeStamp() ); matchedLog.add(logFilterElement); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java index 490219a13d9..ef4e958ae44 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/BuildArguments.java @@ -4,8 +4,10 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramQuantityIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramStringIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; import com.google.protobuf.ByteString; +import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,9 +15,11 @@ import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.tron.api.GrpcAPI.BytesMessage; +import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil.HexMode; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; @@ -24,6 +28,16 @@ @ToString public class BuildArguments { + /** + * Conflict error message wording. Mirrors go-ethereum's + * {@code setDefaults} verbatim — external EVM tooling may + * pattern-match this string. Do not change the wording without + * coordinating with downstream consumers. + */ + private static final String CONFLICT_ERR_MSG = + "both \"data\" and \"input\" are set and not equal. " + + "Please use \"input\" to pass transaction call data"; + @Getter @Setter private String from; @@ -44,6 +58,9 @@ public class BuildArguments { private String data; @Getter @Setter + private String input; + @Getter + @Setter private String nonce = ""; //not used @Getter @@ -83,16 +100,50 @@ public BuildArguments(CallArguments args) { gasPrice = args.getGasPrice(); value = args.getValue(); data = args.getData(); + input = args.getInput(); + } + + /** + * Returns {@code input} if non-null, else {@code data}. Pure + * precedence resolution, mirroring go-ethereum's + * {@code TransactionArgs.data()}. + * + *

Both fields are first validated by + * {@link org.tron.core.services.jsonrpc.JsonRpcApiUtil#requireValidHex} + * — strict for {@code input}, lenient for {@code data} (see that + * method for the rules). + * + *

Conflict between {@code input} and {@code data} is not checked + * here. Build-path callers must route through + * {@link #getContractType(Wallet)} for the geth-equivalent + * {@code setDefaults} enforcement. + * + *

Java callers using positional constructors should pass + * {@code null} (not {@code ""}) for unset {@code input}. + * + *

Verb-prefix name (not {@code getXxx}) keeps Jackson and + * FastJSON's JavaBean introspection from invoking it during + * serialisation; two regression tests per DTO pin this invariant. + */ + public String resolveData() throws JsonRpcInvalidParamsException { + requireValidHex("input", input, HexMode.STRICT); + requireValidHex("data", data, HexMode.LENIENT); + return input != null ? input : data; } public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + // Fail fast on bad hex / conflict before the state lookup; + // calldataEquals relies on resolveData() having validated hex first. + String resolvedData = resolveData(); + validateCallDataConflict(); + ContractType contractType; // to is null if (paramStringIsNull(to)) { // data is null - if (paramStringIsNull(data)) { + if (paramStringIsNull(resolvedData)) { throw new JsonRpcInvalidRequestException("invalid json request"); } @@ -136,4 +187,22 @@ private boolean availableTransferAsset() { return tokenId > 0 && tokenValue > 0 && paramQuantityIsNull(value); } + /** + * Throws when both fields decode to non-equal bytes. Wording matches + * geth's setDefaults so existing tooling can detect the error string. + */ + private void validateCallDataConflict() throws JsonRpcInvalidParamsException { + if (input != null && data != null && !calldataEquals(input, data)) { + throw new JsonRpcInvalidParamsException(CONFLICT_ERR_MSG); + } + } + + /** + * Byte-level equality, so {@code "0xDEAD"} equals {@code "0xdead"}. Both + * args must have passed {@code requireValidHex} first. + */ + private static boolean calldataEquals(String a, String b) { + return Arrays.equals(ByteArray.fromHexString(a), ByteArray.fromHexString(b)); + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java index 70edd1ad94f..1715636a2a4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/CallArguments.java @@ -3,6 +3,7 @@ import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.addressCompatibleToByteArray; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.paramStringIsNull; import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseQuantityValue; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.requireValidHex; import com.google.protobuf.ByteString; import lombok.AllArgsConstructor; @@ -15,6 +16,7 @@ import org.tron.core.Wallet; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.exception.jsonrpc.JsonRpcInvalidRequestException; +import org.tron.core.services.jsonrpc.JsonRpcApiUtil.HexMode; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; @@ -43,21 +45,41 @@ public class CallArguments { private String data; @Getter @Setter + private String input; + @Getter + @Setter private String nonce; // not used + /** + * Returns {@code input} if non-null, else {@code data}. Pure + * precedence resolution, mirroring go-ethereum's + * {@code TransactionArgs.data()}; no conflict check on the query + * path (matches geth's {@code ToMessage}). See + * {@link BuildArguments#resolveData()} for the rationale on + * naming, validation split, and serialiser interaction. + */ + public String resolveData() throws JsonRpcInvalidParamsException { + requireValidHex("input", input, HexMode.STRICT); + requireValidHex("data", data, HexMode.LENIENT); + return input != null ? input : data; + } + /** * just support TransferContract, CreateSmartContract and TriggerSmartContract * */ public ContractType getContractType(Wallet wallet) throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { - ContractType contractType; - // from or to is null if (paramStringIsNull(from)) { throw new JsonRpcInvalidRequestException("invalid json request"); - } else if (paramStringIsNull(to)) { + } + // Fail fast on bad hex before the state lookup. + String resolvedData = resolveData(); + + ContractType contractType; + if (paramStringIsNull(to)) { // data is null - if (paramStringIsNull(data)) { + if (paramStringIsNull(resolvedData)) { throw new JsonRpcInvalidRequestException("invalid json request"); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java index fd57ec0d9ad..6c22c1560e4 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionReceipt.java @@ -35,6 +35,7 @@ public static class TransactionLog { private String data; private String[] topics; private boolean removed = false; + private String blockTimestamp; public TransactionLog() {} } @@ -108,6 +109,7 @@ public TransactionReceipt( // Set logs List logList = new ArrayList<>(); + String blockTimestamp = ByteArray.toJsonHex(blockCapsule.getTimeStamp() / 1000); for (int logIndex = 0; logIndex < txInfo.getLogCount(); logIndex++) { TransactionInfo.Log log = txInfo.getLogList().get(logIndex); TransactionLog transactionLog = new TransactionLog(); @@ -116,6 +118,7 @@ public TransactionReceipt( transactionLog.setTransactionIndex(this.transactionIndex); transactionLog.setBlockHash(this.blockHash); transactionLog.setBlockNumber(this.blockNumber); + transactionLog.setBlockTimestamp(blockTimestamp); byte[] addressByte = convertToTronAddress(log.getAddress().toByteArray()); transactionLog.setAddress(ByteArray.toJsonHexAddress(addressByte)); diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java index 57650355d46..4f11c1a5908 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/types/TransactionResult.java @@ -98,7 +98,7 @@ public TransactionResult(BlockCapsule blockCapsule, int index, Protocol.Transact TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = ByteArray.toJsonHex(blockCapsule.getBlockId().getBytes()); blockNumber = ByteArray.toJsonHex(blockCapsule.getNum()); transactionIndex = ByteArray.toJsonHex(index); @@ -133,7 +133,7 @@ public TransactionResult(Transaction tx, Wallet wallet) { TransactionCapsule capsule = new TransactionCapsule(tx); byte[] txId = capsule.getTransactionId().getBytes(); hash = ByteArray.toJsonHex(txId); - nonce = ByteArray.toJsonHex(new byte[8]); // no value + nonce = "0x0"; // no value, QUANTITY type per Ethereum JSON-RPC spec blockHash = "0x"; blockNumber = "0x"; transactionIndex = "0x"; 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/core/zen/ShieldedTRC20ParametersBuilder.java b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java index 95e4eeb0ccd..4b980c7b7c9 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -14,6 +14,7 @@ import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.BytesMessage; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; @@ -547,8 +548,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -558,8 +559,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -570,23 +571,23 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public static class SpendDescriptionInfo { diff --git a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java index 2e531e44d44..fc3be8352ee 100644 --- a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java @@ -10,6 +10,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.zksnark.IncrementalMerkleVoucherContainer; import org.tron.common.zksnark.JLibrustzcash; @@ -66,8 +67,8 @@ public ZenTransactionBuilder() { } public void addSpend(SpendDescriptionInfo spendDescriptionInfo) { + valueBalance = StrictMathWrapper.addExact(valueBalance, spendDescriptionInfo.note.getValue()); spends.add(spendDescriptionInfo); - valueBalance += spendDescriptionInfo.note.getValue(); } public void addSpend( @@ -75,8 +76,8 @@ public void addSpend( Note note, byte[] anchor, IncrementalMerkleVoucherContainer voucher) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -85,8 +86,8 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -97,23 +98,23 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, ovk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void setTransparentInput(byte[] address, long value) { 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..296a4d4b32a 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -178,7 +178,13 @@ node { minParticipationRate = 15 - # allowShieldedTransactionApi = true + # WARNING: Some shielded transaction APIs require sending private keys as parameters. + # Calling these APIs on untrusted or remote nodes may leak your private keys. + # It is recommended to invoke them locally for development and testing. + # To opt in, set: allowShieldedTransactionApi = true + # Migration: the legacy key node.fullNodeAllowShieldedTransaction is still supported + # but deprecated; please migrate to node.allowShieldedTransactionApi. + # allowShieldedTransactionApi = false # openPrintLog = true @@ -223,6 +229,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 +260,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 +372,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/main/resources/logback.xml b/framework/src/main/resources/logback.xml index 03d870e92e0..1b0955df2fd 100644 --- a/framework/src/main/resources/logback.xml +++ b/framework/src/main/resources/logback.xml @@ -2,6 +2,14 @@ + + + true + + + + ./logs/grpc/grpc.log + + ./logs/grpc/grpc-%d{yyyy-MM-dd}.%i.log.gz + 500MB + 7 + 50GB + + + %d{HH:mm:ss.SSS} %-5level [%t] [%c{1}] %m%n + + + TRACE + + + + + 1024 + 0 + true + 5000 + + + 0 @@ -61,19 +93,17 @@ 100 true + + 5000 - - - - - - - + + + @@ -88,6 +118,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -97,6 +150,8 @@ + + + - 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/log/LogServiceTest.java b/framework/src/test/java/org/tron/common/log/LogServiceTest.java new file mode 100644 index 00000000000..3ac00a9e599 --- /dev/null +++ b/framework/src/test/java/org/tron/common/log/LogServiceTest.java @@ -0,0 +1,138 @@ +package org.tron.common.log; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.jul.LevelChangePropagator; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; +import org.tron.core.exception.TronError; + +/** + * Verifies that {@link LogService#load(String)} keeps the Logback<->JUL level + * bridge working even when the active configuration does not declare a + * {@code LevelChangePropagator} itself. + */ +public class LogServiceTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void restoreDefaultLogbackConfig() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + lc.reset(); + try { + new ContextInitializer(lc).autoConfig(); + } catch (JoranException e) { + Assert.fail("failed to restore default logback config: " + e.getMessage()); + } + } + + @Test + public void propagatorIsInstalledWhenCustomConfigOmitsIt() throws IOException { + Path xml = writeLogbackXml("DEBUG", false); + + LogService.load(xml.toString()); + + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + assertEquals(1, countLevelChangePropagators(lc)); + + // LevelChangePropagator maps Logback DEBUG -> JUL FINE. + Level julLevel = Logger.getLogger("io.grpc").getLevel(); + assertNotNull("JUL level for io.grpc should be synced from Logback", julLevel); + assertEquals(Level.FINE, julLevel); + } + + @Test + public void propagatorIsNotDuplicatedWhenCustomConfigDeclaresIt() throws IOException { + Path xml = writeLogbackXml("INFO", true); + + LogService.load(xml.toString()); + + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + assertEquals("XML-declared propagator should not be duplicated", + 1, countLevelChangePropagators(lc)); + } + + @Test + public void propagatorIsEnsuredWhenNoLogConfigIsSupplied() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + // Drop whatever the default logback-test.xml registered so we can observe the + // fall-through path (no --log-config) installing the propagator on its own. + removeLevelChangePropagators(lc); + assertEquals(0, countLevelChangePropagators(lc)); + + // Empty path == no --log-config passed; must keep classpath default AND + // still install the propagator so JUL sync works. + LogService.load(""); + + assertEquals("ensureLevelChangePropagator should run on the default context", + 1, countLevelChangePropagators(lc)); + } + + @Test + public void nonEmptyInvalidPathFailsFast() { + // A non-empty --log-config that cannot be read must surface loudly instead + // of silently falling back to the classpath default. + TronError thrown = assertThrows(TronError.class, + () -> LogService.load("definitely-not-a-real-path.xml")); + assertEquals(TronError.ErrCode.LOG_LOAD, thrown.getErrCode()); + } + + private Path writeLogbackXml(String level, + boolean includePropagator) throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + if (includePropagator) { + sb.append(" \n"); + sb.append(" true\n"); + sb.append(" \n"); + } + sb.append(" \n"); + sb.append(" %m%n\n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append("\n"); + Path path = temporaryFolder.newFile("logback.xml").toPath(); + Files.write(path, sb.toString().getBytes(StandardCharsets.UTF_8)); + return path; + } + + private static int countLevelChangePropagators(LoggerContext lc) { + int count = 0; + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + count++; + } + } + return count; + } + + private static void removeLevelChangePropagators(LoggerContext lc) { + for (LoggerContextListener listener : lc.getCopyOfListenerList()) { + if (listener instanceof LevelChangePropagator) { + lc.removeListener(listener); + } + } + } +} 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..8e6b366fef8 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/EventParserTest.java @@ -5,6 +5,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.bouncycastle.crypto.OutputLengthException; import org.bouncycastle.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -68,7 +69,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; @@ -101,6 +101,91 @@ public synchronized void testEventParser() { } + @Test + public void testParseDataBytesIntegerTypes() { + // uint256 = 255 + byte[] uintData = ByteArray.fromHexString( + "00000000000000000000000000000000000000000000000000000000000000ff"); + Assert.assertEquals("255", ContractEventParser.parseDataBytes(uintData, "uint256", 0)); + + // int256 = -1 (two's complement 0xFF..FF is signed negative one) + byte[] negIntData = ByteArray.fromHexString( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + Assert.assertEquals("-1", ContractEventParser.parseDataBytes(negIntData, "int256", 0)); + + // trcToken is classified as INT_NUMBER + byte[] tokenData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000064"); + Assert.assertEquals("100", ContractEventParser.parseDataBytes(tokenData, "trcToken", 0)); + } + + @Test + public void testParseDataBytesBool() { + byte[] trueData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("true", ContractEventParser.parseDataBytes(trueData, "bool", 0)); + + byte[] falseData = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("false", ContractEventParser.parseDataBytes(falseData, "bool", 0)); + } + + @Test + public void testParseDataBytesFixedBytes() { + String hex = "1234567890abcdef0000000000000000000000000000000000000000000000ff"; + byte[] data = ByteArray.fromHexString(hex); + Assert.assertEquals(hex, ContractEventParser.parseDataBytes(data, "bytes32", 0)); + } + + @Test + public void testParseDataBytesAddress() { + Wallet.setAddressPreFixByte(ADD_PRE_FIX_BYTE_MAINNET); + // last 20 bytes = ca35...733c => Base58Check = TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt + byte[] data = ByteArray.fromHexString( + "000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c"); + Assert.assertEquals("TUQPrDEJkV4ttkrL7cVv1p3mikWYfM7LWt", + ContractEventParser.parseDataBytes(data, "address", 0)); + } + + @Test + public void testParseDataBytesDynamicBytes() { + // offset 0x20 | length 3 | 0x010203 padded to 32 bytes + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0102030000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("010203", ContractEventParser.parseDataBytes(data, "bytes", 0)); + } + + @Test + public void testParseDataBytesEmptyString() { + // offset 0x20 | length 0 + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesNonEmptyString() { + // "hello world" is 11 ASCII bytes (68656c6c6f20776f726c64), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000000000000b" + + "68656c6c6f20776f726c64000000000000000000000000000000000000000000"); + Assert.assertEquals("hello world", ContractEventParser.parseDataBytes(data, "string", 0)); + } + + @Test + public void testParseDataBytesMultiByteUtf8String() { + // "中文" UTF-8 = e4b8ad e69687 (6 bytes), padded to 32 bytes. + byte[] data = ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000006" + + "e4b8ade696870000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("中文", ContractEventParser.parseDataBytes(data, "string", 0)); + } + @Test public void testParseRevert() { String dataHex = "08c379a0" @@ -114,4 +199,87 @@ public void testParseRevert() { Assert.assertEquals(msg, "not enough input value"); } + + @Test + public void testSubBytesRejectsOversizedLength() { + // Length must fit in the available source bytes. Reject instead of + // truncating so oversized ABI lengths are not silently coerced. + byte[] src = new byte[]{1, 2, 3}; + try { + ContractEventParser.subBytes(src, 0, Integer.MAX_VALUE); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:0")); + Assert.assertTrue(e.getMessage().contains("length:2147483647")); + Assert.assertTrue(e.getMessage().contains("src.length:3")); + } + } + + @Test + public void testSubBytesAcceptsExactLength() { + byte[] src = new byte[]{1, 2, 3, 4}; + byte[] result = ContractEventParser.subBytes(src, 1, 3); + Assert.assertArrayEquals(new byte[]{2, 3, 4}, result); + } + + @Test + public void testSubBytesRejectsNegativeOffset() { + // ABI offsets are unsigned, but BigInteger(byte[]) interprets 0xFF..FF as + // -1. The guard should reject that value before System.arraycopy runs. + byte[] src = new byte[]{1, 2, 3, 4}; + try { + ContractEventParser.subBytes(src, -1, 3); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("data start:-1")); + Assert.assertTrue(e.getMessage().contains("length:3")); + Assert.assertTrue(e.getMessage().contains("src.length:4")); + } + } + + @Test + public void testSubBytesRejectsEmptySource() { + try { + ContractEventParser.subBytes(new byte[0], 0, 0); + Assert.fail("Expected OutputLengthException"); + } catch (OutputLengthException e) { + Assert.assertTrue(e.getMessage().contains("source data is empty")); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeOffset() { + // End-to-end check: an offset field of 0xFF..FF decodes to -1 and should + // be rejected through the existing UnsupportedOperationException path. + String dataHex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsMalformedLength() { + // ABI-encoded "string" whose declared length exceeds the available payload + // should be rejected via the existing UnsupportedOperationException path. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } + + @Test(expected = UnsupportedOperationException.class) + public void testParseDataBytesRejectsNegativeLength() { + // ABI length is an unsigned word. If 0xFF..FF is decoded as -1, reject it + // instead of treating it as an empty string/bytes payload. + String dataHex = "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"; + byte[] data = ByteArray.fromHexString(dataHex); + + ContractEventParser.parseDataBytes(data, "string", 0); + } } 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/prometheus/SRMetricsTest.java b/framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java new file mode 100644 index 00000000000..4c2e9292d29 --- /dev/null +++ b/framework/src/test/java/org/tron/common/prometheus/SRMetricsTest.java @@ -0,0 +1,206 @@ +package org.tron.common.prometheus; + +import com.google.protobuf.ByteString; +import io.prometheus.client.CollectorRegistry; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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.utils.StringUtil; +import org.tron.consensus.dpos.MaintenanceManager; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.VotesCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.consensus.ConsensusService; +import org.tron.protos.Protocol; +import org.tron.protos.Protocol.Vote; + +@Slf4j(topic = "metric") +public class SRMetricsTest extends BaseTest { + + private static final AtomicInteger PORT = new AtomicInteger(0); + private static final AtomicInteger UNIQUE = new AtomicInteger(0); + + @Resource + private MaintenanceManager maintenanceManager; + @Resource + private ConsensusService consensusService; + + static { + Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); + Args.getInstance().setNodeListenPort(20000 + PORT.incrementAndGet()); + Args.getInstance().setMetricsPrometheusEnable(true); + Metrics.init(); + } + + @Before + public void setUp() { + Args.getInstance().setMetricsPrometheusEnable(true); + consensusService.start(); + } + + @After + public void tearDown() { + Args.getInstance().setMetricsPrometheusEnable(true); + } + + /** + * Drive the full maintenance flow: starting with a single active witness while WitnessStore + * contains additional ones, doMaintenance() should expand active witnesses to the full set and + * emit SR_ADD for each newly active witness. + */ + @Test + public void testSrAddViaMaintenance() { + ByteString stableWit = registerWitness(); + ByteString newWit1 = registerWitness(); + ByteString newWit2 = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + seedVote(stableWit); + + maintenanceManager.doMaintenance(); + + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_ADD, newWit1).intValue()); + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_ADD, newWit2).intValue()); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, stableWit)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, stableWit)); + } + + /** + * Active witness set already matches WitnessStore → no metric emitted. + */ + @Test + public void testNoMetricWhenSetUnchanged() { + ByteString witA = registerWitness(); + ByteString witB = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Arrays.asList(witA, witB)); + + seedVote(witA); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, witA)); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, witB)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, witA)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, witB)); + } + + /** + * Empty VotesStore → countVote() is empty → SR change check is skipped, even when the active + * set differs from the full witness store. + */ + @Test + public void testNoMetricWhenNoVotes() { + ByteString stableWit = registerWitness(); + ByteString newWit = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, newWit)); + } + + /** + * Metrics disabled → record() short-circuits even though the active set changes. + */ + @Test + public void testNoMetricWhenMetricsDisabled() { + Args.getInstance().setMetricsPrometheusEnable(false); + try { + ByteString stableWit = registerWitness(); + ByteString newWit = registerWitness(); + + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(stableWit)); + + seedVote(stableWit); + + maintenanceManager.doMaintenance(); + + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, newWit)); + } finally { + Args.getInstance().setMetricsPrometheusEnable(true); + } + } + + /** + * SR_REMOVE is verified by directly calling record() instead of going through doMaintenance(), + * because driving a removal through the real flow is impractical here: + * + *

Inside doMaintenance(), the block before SRMetrics.recordSrSetChange() iterates currentWits + * and calls setIsJobs(false) on each WitnessCapsule fetched from WitnessStore. If currentWits + * contains any address that is not present in WitnessStore, getWitness() returns null and the + * code NPEs — so SR_REMOVE cannot be triggered by simply pointing the active set at an + * "obsolete" address. + * + *

The only other path to SR_REMOVE is rank-based eviction: with more than + * MAX_ACTIVE_WITNESS_NUM (27) witnesses, sorting drops the lowest-ranked one. Building that + * setup just to exercise this branch is heavy and adds little value, since SR_ADD and + * SR_REMOVE share the exact same emit logic in record() — verifying SR_ADD via doMaintenance + * already proves the wiring is correct, and this direct call covers the symmetric branch. + */ + @Test + public void testSrRemoveDirect() { + ByteString stableWit = uniqueAddress(); + ByteString removedWit = uniqueAddress(); + + SRMetrics.recordSrSetChange( + Arrays.asList(stableWit, removedWit), + Collections.singletonList(stableWit)); + + Assert.assertEquals(1, sample(MetricLabels.Counter.SR_REMOVE, removedWit).intValue()); + Assert.assertNull(sample(MetricLabels.Counter.SR_ADD, removedWit)); + Assert.assertNull(sample(MetricLabels.Counter.SR_REMOVE, stableWit)); + } + + private ByteString registerWitness() { + ByteString address = uniqueAddress(); + chainBaseManager.getWitnessStore().put(address.toByteArray(), new WitnessCapsule(address)); + chainBaseManager.addWitness(address); + chainBaseManager.getAccountStore().put(address.toByteArray(), + new AccountCapsule(Protocol.Account.newBuilder().setAddress(address).build())); + return address; + } + + private void seedVote(ByteString voteFor) { + ByteString voter = uniqueAddress(); + VotesCapsule votes = new VotesCapsule(voter, Collections.emptyList(), + Collections.singletonList(Vote.newBuilder() + .setVoteAddress(voteFor) + .setVoteCount(1L) + .build())); + chainBaseManager.getVotesStore().put(voter.toByteArray(), votes); + } + + private ByteString uniqueAddress() { + int n = UNIQUE.incrementAndGet(); + byte[] bytes = new byte[21]; + bytes[0] = 0x41; + bytes[17] = (byte) ((n >> 16) & 0xFF); + bytes[18] = (byte) ((n >> 8) & 0xFF); + bytes[19] = (byte) (n & 0xFF); + bytes[20] = 0x01; + return ByteString.copyFrom(bytes); + } + + private Double sample(String action, ByteString witness) { + return CollectorRegistry.defaultRegistry.getSampleValue( + MetricKeys.Counter.SR_SET_CHANGE + "_total", + new String[]{"action", "witness"}, + new String[]{action, StringUtil.encode58Check(witness.toByteArray())}); + } +} 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/storage/leveldb/LevelDbDataSourceImplTest.java b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java index 7fd792a958d..41e8749e1ec 100644 --- a/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java +++ b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java @@ -19,10 +19,20 @@ package org.tron.common.storage.leveldb; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; @@ -32,11 +42,13 @@ import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.rocksdb.RocksDB; +import org.slf4j.LoggerFactory; import org.tron.common.TestConstants; import org.tron.common.parameter.CommonParameter; import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; import org.tron.common.utils.FileUtil; import org.tron.common.utils.PropUtil; +import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.StorageUtils; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; @@ -122,4 +134,59 @@ private void makeExceptionDb(String dbName) { FileUtil.saveData(dataSource.getDbPath().toString() + "/CURRENT", "...", Boolean.FALSE); } + + @Test + public void slowOpen() throws IOException { + Logger dbLogger = (Logger) LoggerFactory.getLogger("DB"); + ListAppender dbAppender = new ListAppender<>(); + dbAppender.start(); + dbLogger.addAppender(dbAppender); + try { + final File dbDir = temporaryFolder.newFolder(); + final Path dbPath = dbDir.toPath(); + final String watchdogDbName = "slow-open-db"; + + LevelDbDataSourceImpl ds = new LevelDbDataSourceImpl(); + ReflectUtils.setFieldValue(ds, "dataBaseName", watchdogDbName); + ReflectUtils.setFieldValue(ds, "parentPath", dbDir.getParent()); + long startNs = System.nanoTime() - TimeUnit.SECONDS.toNanos(61); + ds.logSlowOpen(dbPath, startNs); + + List warns = dbAppender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .collect(Collectors.toList()); + assertEquals("expected exactly one WARN event", 1, warns.size()); + ILoggingEvent warn = warns.get(0); + assertNotNull("expected one WARN from the watchdog helper", warn); + String rendered = warn.getFormattedMessage(); + assertTrue("WARN should include the Toolkit remediation hint: " + rendered, + rendered.contains("Toolkit.jar db archive -d")); + assertTrue("WARN should echo the db name: " + rendered, + rendered.contains(watchdogDbName)); + } finally { + dbAppender.stop(); + dbLogger.detachAppender(dbAppender); + } + } + + @Test + public void fastOpen() { + Logger dbLogger = (Logger) LoggerFactory.getLogger("DB"); + ListAppender dbAppender = new ListAppender<>(); + dbAppender.start(); + dbLogger.addAppender(dbAppender); + try { + String dir = Args.getInstance().getOutputDirectory() + + Args.getInstance().getStorage().getDbDirectory(); + LevelDbDataSourceImpl ds = new LevelDbDataSourceImpl(dir, "test_fast_open"); + ds.closeDB(); + long warnCount = dbAppender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .count(); + assertEquals("no WARN should fire for a fast open", 0, warnCount); + } finally { + dbAppender.stop(); + dbLogger.detachAppender(dbAppender); + } + } } 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..0353d260eff 100644 --- a/framework/src/test/java/org/tron/core/ShieldWalletTest.java +++ b/framework/src/test/java/org/tron/core/ShieldWalletTest.java @@ -4,15 +4,18 @@ import static org.mockito.Mockito.spy; import static org.tron.core.zen.ZksnarkInitService.librustzcashInitZksnarkParams; +import com.google.protobuf.ByteString; import java.math.BigInteger; import javax.annotation.Resource; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.PrivateParameters; import org.tron.api.GrpcAPI.PrivateParametersWithoutAsk; import org.tron.api.GrpcAPI.PrivateShieldedTRC20Parameters; import org.tron.api.GrpcAPI.PrivateShieldedTRC20ParametersWithoutAsk; +import org.tron.api.GrpcAPI.ReceiveNote; import org.tron.api.GrpcAPI.ShieldedAddressInfo; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; import org.tron.common.BaseTest; @@ -22,6 +25,7 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ZksnarkException; import org.tron.core.services.http.JsonFormat; import org.tron.core.services.http.JsonFormat.ParseException; @@ -369,14 +373,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 +404,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 @@ -454,4 +454,226 @@ public void testCreateShieldedContractParametersWithoutAsk() throws ContractExeE Assert.fail(); } } + + private static final byte[] SHIELDED_CONTRACT_ADDRESS = + ByteArray.fromHexString("4144007979359ECAC395BBD3CEF8060D3DF2DC3F01"); + private static final String VALID_PAYMENT_ADDR = + "ztron1y99u6ejqenupvfkp5g6q6yqkp0a44c48cta0dd5gejtqa4v27hqa2cghfvdxnmneh6qqq03fa75"; + + private Wallet newSpyWallet() throws ContractExeException { + Args.getInstance().setAllowShieldedTransactionApi(true); + Wallet wallet1 = spy(new Wallet()); + doReturn(BigInteger.valueOf(1).toByteArray()) + .when(wallet1).getShieldedContractScalingFactor(SHIELDED_CONTRACT_ADDRESS); + return wallet1; + } + + private GrpcAPI.SpendNoteTRC20 spendNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return GrpcAPI.SpendNoteTRC20.newBuilder() + .setNote(note) + .setAlpha(ByteString.copyFrom(new byte[32])) + .setRoot(ByteString.copyFrom(new byte[32])) + .setPath(ByteString.copyFrom(new byte[1024])) + .setPos(0) + .build(); + } + + private ReceiveNote receiveNoteOfValue(long value) { + GrpcAPI.Note note = GrpcAPI.Note.newBuilder() + .setValue(value) + .setPaymentAddress(VALID_PAYMENT_ADDR) + .setRcm(ByteString.copyFrom(new byte[32])) + .setMemo(ByteString.copyFrom(new byte[512])) + .build(); + return ReceiveNote.newBuilder().setNote(note).build(); + } + + @Test + public void testCreateShieldedContractParameters_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ask, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No valid transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParameters_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20Parameters request = PrivateShieldedTRC20Parameters.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAsk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParameters(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_invalidParams() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("invalid shielded TRC-20 parameters", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingKeys() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No shielded TRC-20 ak, nsk or ovk", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_BURN_missingTransparentTo() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(100)) + .setToAmount("100") + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ContractValidateException); + Assert.assertEquals("No transparent TRC-20 output address", e.getMessage()); + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_MINT_emptyOvk() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .setFromAmount("100") + .addShieldedReceives(receiveNoteOfValue(100)) + .build(); + try { + ShieldedTRC20Parameters params = wallet1.createShieldedContractParametersWithoutAsk(request); + Assert.assertNotNull(params); + } catch (Exception e) { + Assert.fail("MINT with empty ovk should auto-generate one: " + e); + } + } + + @Test + public void testCreateShieldedContractParametersWithoutAsk_TRANSFER_arithmeticOverflow() + throws ContractExeException { + Wallet wallet1 = newSpyWallet(); + PrivateShieldedTRC20ParametersWithoutAsk request = + PrivateShieldedTRC20ParametersWithoutAsk.newBuilder() + .setShieldedTRC20ContractAddress(ByteString.copyFrom(SHIELDED_CONTRACT_ADDRESS)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedSpends(spendNoteOfValue(Long.MAX_VALUE)) + .addShieldedReceives(receiveNoteOfValue(0)) + .setAk(ByteString.copyFrom(new byte[32])) + .setNsk(ByteString.copyFrom(new byte[32])) + .setOvk(ByteString.copyFrom(new byte[32])) + .build(); + Exception e = Assert.assertThrows(Exception.class, + () -> wallet1.createShieldedContractParametersWithoutAsk(request)); + Assert.assertTrue(e instanceof ZksnarkException); + Assert.assertEquals("shielded amount overflow", e.getMessage()); + } } diff --git a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java index c2c4bfe3006..71192706049 100644 --- a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java +++ b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.util.encoders.Hex; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Ignore; @@ -73,9 +74,18 @@ public class ShieldedTRC20BuilderTest extends BaseTest { VerifyTransferProof transferContract = new VerifyTransferProof(); VerifyBurnProof burnContract = new VerifyBurnProof(); + private static boolean origShieldedApi; + @BeforeClass public static void initZksnarkParams() { ZksnarkInitService.librustzcashInitZksnarkParams(); + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; } @Ignore 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/ShieldedTransferActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java index faec4c74039..578f9f5ebed 100755 --- a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java @@ -1157,6 +1157,9 @@ public void publicAddressToShieldNoteValueFailure() { actuator.validate(); actuator.execute(ret); Assert.assertTrue(false); + } catch (ArithmeticException e) { + // StrictMathWrapper.subtractExact throws ArithmeticException on overflow + Assert.assertTrue(true); } catch (ContractValidateException e) { Assert.assertTrue(e instanceof ContractValidateException); Assert.assertEquals("librustzcashSaplingFinalCheck error", e.getMessage()); @@ -1346,5 +1349,58 @@ public void shieldAddressToPublic() { Assert.assertTrue(false); } } + + /** + * Test that shielded transfer transaction validation works even when + * allowShieldedTransactionApi is disabled. This verifies that the API flag + * only gates wallet/helper APIs, not the core transaction validation logic. + */ + @Test + public void shieldedTransferValidationWorksWhenApiDisabled() { + boolean orig = Args.getInstance().isAllowShieldedTransactionApi(); + // Disable the shielded API (this should NOT affect transaction validation) + Args.getInstance().setAllowShieldedTransactionApi(false); + + dbManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); + dbManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(AMOUNT); + + try { + ZenTransactionBuilder builder = new ZenTransactionBuilder(wallet); + SpendingKey sk = SpendingKey.random(); + ExpandedSpendingKey expsk = sk.expandedSpendingKey(); + PaymentAddress address = sk.defaultAddress(); + Note note = new Note(address, AMOUNT); + IncrementalMerkleVoucherContainer voucher = createSimpleMerkleVoucherContainer(note.cm()); + byte[] anchor = voucher.root().getContent().toByteArray(); + dbManager.getMerkleContainer() + .putMerkleTreeIntoStore(anchor, voucher.getVoucherCapsule().getTree()); + builder.addSpend(expsk, note, anchor, voucher); + + addZeroValueOutputNote(builder); + + long fee = dbManager.getDynamicPropertiesStore().getShieldedTransactionCreateAccountFee(); + String addressNotExist = + Wallet.getAddressPreFixString() + "8ba2aaae540c642e44e3bed5522c63bbc21f0000"; + + builder.setTransparentOutput(ByteArray.fromHexString(addressNotExist), AMOUNT - fee); + + TransactionCapsule transactionCap = builder.build(); + Contract contract = + transactionCap.getInstance().toBuilder().getRawDataBuilder().getContract(0); + ShieldedTransferActuator actuator = new ShieldedTransferActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()).setContract(contract) + .setTx(transactionCap); + + // Validation should succeed even when API is disabled + actuator.validate(); + } catch (ContractValidateException e) { + Assert.fail("Shielded transfer validation should not throw ContractValidateException: " + + e.getMessage()); + } catch (Exception e) { + Assert.fail("Shielded transfer should not throw Exception: " + e.getMessage()); + } finally { + Args.getInstance().setAllowShieldedTransactionApi(orig); + } + } } 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..2a9de1ea3b1 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(); @@ -448,6 +343,8 @@ public void validateCheck() { testAllowTvmSelfdestructRestrictionProposal(); + testAllowHardenResourceCalculationProposal(); + forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats); forkUtils.reset(); @@ -455,15 +352,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 +374,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 +395,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 +426,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 +447,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 +469,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 +490,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 +512,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 +533,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 +555,62 @@ 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 testAllowHardenResourceCalculationProposal() { + byte[] stats = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]", + e1.getMessage()); + + long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() + .getMaintenanceTimeInterval(); + + long hardForkTime = + ((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime + 1); + + stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + + // Should fail because the proposal value is invalid + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1", + e2.getMessage()); + + dynamicPropertiesStore.saveAllowHardenResourceCalculation(1); + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, no need to propose again", + e3.getMessage()); } private void testAllowMarketTransaction() { diff --git a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java index 70434430262..9c2e004931e 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -4,12 +4,20 @@ import static org.tron.protos.Protocol.Transaction.Result.contractResult.PRECOMPILED_CONTRACT; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import com.google.protobuf.ByteString; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.LoggerFactory; import org.tron.common.BaseTest; import org.tron.common.TestConstants; import org.tron.common.utils.StringUtil; @@ -69,4 +77,61 @@ public void testRemoveRedundantRet() { Assert.assertEquals(1, transactionCapsule.getInstance().getRetCount()); Assert.assertEquals(SUCCESS, transactionCapsule.getInstance().getRet(0).getContractRet()); } -} \ No newline at end of file + + @Test + public void slowVerify() { + Logger capsuleLogger = (Logger) LoggerFactory.getLogger("capsule"); + Level originalLevel = capsuleLogger.getLevel(); + capsuleLogger.setLevel(Level.INFO); + ListAppender appender = new ListAppender<>(); + appender.start(); + capsuleLogger.addAppender(appender); + try { + TransactionCapsule cap = new TransactionCapsule(Transaction.newBuilder().build()); + long startNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(51); + cap.logSlowSigVerify(startNs); + + List warns = appender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .collect(Collectors.toList()); + Assert.assertEquals("expected one WARN for a slow verify", 1, warns.size()); + String rendered = warns.get(0).getFormattedMessage(); + Assert.assertTrue("WARN should mention slow verify: " + rendered, + rendered.contains("slow verify")); + Assert.assertTrue("WARN should echo the txId: " + rendered, + rendered.contains(cap.getTransactionId().toString())); + Assert.assertTrue("WARN should include sigCount: " + rendered, + rendered.contains("sigCount=")); + Assert.assertTrue("WARN should include cost in ms: " + rendered, + rendered.contains("cost=")); + Assert.assertTrue("WARN should render ms suffix: " + rendered, + rendered.contains(" ms")); + } finally { + appender.stop(); + capsuleLogger.detachAppender(appender); + capsuleLogger.setLevel(originalLevel); + } + } + + @Test + public void fastVerify() { + Logger capsuleLogger = (Logger) LoggerFactory.getLogger("capsule"); + Level originalLevel = capsuleLogger.getLevel(); + capsuleLogger.setLevel(Level.INFO); + ListAppender appender = new ListAppender<>(); + appender.start(); + capsuleLogger.addAppender(appender); + try { + TransactionCapsule cap = new TransactionCapsule(Transaction.newBuilder().build()); + cap.logSlowSigVerify(System.nanoTime()); + long warnCount = appender.list.stream() + .filter(e -> e.getLevel() == Level.WARN) + .count(); + Assert.assertEquals("no WARN should fire below the threshold", 0, warnCount); + } finally { + appender.stop(); + capsuleLogger.detachAppender(appender); + capsuleLogger.setLevel(originalLevel); + } + } +} 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/TronLogShutdownHookTest.java b/framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java new file mode 100644 index 00000000000..85ade6ba7fa --- /dev/null +++ b/framework/src/test/java/org/tron/core/config/TronLogShutdownHookTest.java @@ -0,0 +1,84 @@ +package org.tron.core.config; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TronLogShutdownHookTest { + + private boolean originalShutDown; + + @Before + public void saveShutDownFlag() { + originalShutDown = TronLogShutdownHook.shutDown; + } + + @After + public void restoreShutDownFlag() { + TronLogShutdownHook.shutDown = originalShutDown; + } + + @Test(timeout = 5_000) + public void returnsImmediatelyWhenAlreadyShutDown() { + TronLogShutdownHook.shutDown = true; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + long startNs = System.nanoTime(); + hook.run(); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + assertTrue("hook should exit fast when shutDown==true, elapsed=" + elapsedMs + "ms", + elapsedMs < 2_000); + } + + @Test(timeout = 10_000) + public void wakesUpWhenShutDownFlagFlips() throws InterruptedException { + TronLogShutdownHook.shutDown = false; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + Thread runner = new Thread(hook, "shutdown-hook-test-runner"); + runner.setDaemon(true); + runner.start(); + + Thread.sleep(300); + long flipNs = System.nanoTime(); + TronLogShutdownHook.shutDown = true; + + runner.join(5_000); + long elapsedAfterFlipMs = + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - flipNs); + + assertFalse("runner should have exited after flag flipped, still alive", + runner.isAlive()); + // The loop sleeps in 100 ms slices, so it should wake up well inside one + // slice's worth of jitter. 1 s is comfortable even on slow CI. + assertTrue("hook should return shortly after flag flip, elapsed=" + + elapsedAfterFlipMs + "ms", elapsedAfterFlipMs < 1_000); + } + + @Test(timeout = 10_000) + public void preservesInterruptStatusWhenInterrupted() throws InterruptedException { + TronLogShutdownHook.shutDown = false; + + TronLogShutdownHook hook = new TronLogShutdownHook(); + AtomicBoolean interruptedAfterRun = new AtomicBoolean(false); + Thread runner = new Thread(() -> { + hook.run(); + interruptedAfterRun.set(Thread.currentThread().isInterrupted()); + }, "shutdown-hook-test-interrupt"); + runner.setDaemon(true); + runner.start(); + + Thread.sleep(200); + runner.interrupt(); + + runner.join(5_000); + assertFalse("runner should have exited after interrupt", runner.isAlive()); + assertTrue("run() must re-assert interrupt status after catching " + + "InterruptedException", interruptedAfterRun.get()); + } +} 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..026af400754 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,233 @@ 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 testAllowShieldedTransactionApiDefault() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + Assert.assertFalse(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(true); + Assert.assertTrue(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(false); + } + + @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/CalculateGlobalLimitHardenTest.java b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java new file mode 100644 index 00000000000..1df362fff42 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java @@ -0,0 +1,346 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import lombok.extern.slf4j.Slf4j; +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.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class CalculateGlobalLimitHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + } + + private EnergyProcessor energyProcessor; + private BandwidthProcessor bandwidthProcessor; + private AccountCapsule ownerCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + energyProcessor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + bandwidthProcessor = new BandwidthProcessor(dbManager.getChainBaseManager()); + } + + @After + public void tearDown() { + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + } + + @Test + public void testGlobalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(1L); + ownerCapsule.setFrozenForEnergy(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.calculateGlobalEnergyLimit(ownerCapsule)); + } + + @Test + public void testGlobalEnergyLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitV2CorrectVsDoublePrecisionLoss() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; // ~9.8e15 + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalEnergyWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalNetLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + ownerCapsule.setFrozenForBandwidth(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(1L); + ownerCapsule.setFrozenForBandwidth(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule)); + } + + + @Test + public void testGlobalNetLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2ExactPrecision() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalNetLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalNetWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalEnergyLimitV2BelowTrxPrecisionMatchesDouble() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION (1_000_000) + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(12L, resultNew); + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2BelowTrxPrecisionMatchesDouble() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertTrue("non-zero proportional result expected", resultNew > 0); + } + + @Test + public void testGlobalEnergyLimitV1NonIntegerRatioParity() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; // not an exact divisor of totalEnergyLimit + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testV1FlooredWeightVsV2FractionalWeight() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 1_500_000L; // 1.5 x TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + // V1 path + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + ownerCapsule.setFrozenForEnergy(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + long v1New = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + // Legacy V1 expectation: floor(1.5) * 25.0 = 1 * 25 = 25 + Assert.assertEquals(25L, v1New); + + // V2 path with the same balance keeps the fractional weight + long v2New = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + // Legacy V2 expectation: 1.5 * 25.0 = 37.5 -> 37 + Assert.assertEquals(37L, v2New); + + // And both must match their respective legacy doubles + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long v1Old = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + long v2Old = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(v1Old, v1New); + Assert.assertEquals(v2Old, v2New); + } + + @Test + public void testGlobalNetLimitV1UsesTotalNetWeightNotLimit() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; // distinct from totalNetLimit + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + ownerCapsule.setFrozenForBandwidth(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + @Test + public void testGlobalNetLimitV2UsesTotalNetWeightNotLimit() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + + @Test + public void testUpdateAdaptiveTotalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(20_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(10_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultOld = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultNew = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUpdateAdaptiveTotalEnergyLimitOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit( + 10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } + + @Test + public void testUpdateAdaptiveLimitMultiplierOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(1_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(Long.MAX_VALUE / 100); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } +} 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/ResourceProcessorHardenTest.java b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java new file mode 100644 index 00000000000..ee096abd382 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java @@ -0,0 +1,281 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +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.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.contract.Common; +import org.tron.protos.contract.Common.ResourceCode; + +@Slf4j +public class ResourceProcessorHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + private static final String RECEIVER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + RECEIVER_ADDRESS = + Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc"; + } + + private EnergyProcessor processor; + private AccountCapsule ownerCapsule; + private AccountCapsule receiverCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + receiverCapsule = new AccountCapsule( + ByteString.copyFromUtf8("receiver"), + ByteString.copyFrom(ByteArray.fromHexString(RECEIVER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(10000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(10_000_000L); + + processor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test + public void testIncreaseNormalValuesConsistent() { + long lastUsage = 1000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; // 24h in slots + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long lastUsage = 70_000_000L; + long usage = 2345L; + long lastTime = 9999L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increase(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + processor.increase(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testIncreaseAcceptsIntermediateOverflowWhenResultFits() { + long lastUsage = Long.MAX_VALUE / 100; // ~9.2e16 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be a valid long", result >= 0); + } + + @Test + public void testIncreaseWithAccountCapsuleConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + + long lastUsage = 5_000_000L; + long usage = 1_000L; + long lastTime = 9990L; + long now = 9995L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowOld = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowNew = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 1000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2ConsistentWithHardening() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 5_000_000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testIncreaseV2OverflowDetected() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17, above threshold + long usage = 1000L; + long lastTime = 9999L; + long now = 10000L; + + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now)); + } + + @Test + public void testLargeButSafeValuesWithHardening() { + long lastUsage = 300_000_000_000L; // 300 billion + long usage = 100L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be positive", result > 0); + } + + private void setupForUnDelegate(long now) { + setupForUnDelegateWithUsage(now, 5_000_000L, 3_000_000L); + } + + private void setupForUnDelegateWithUsage(long now, long ownerUsage, long receiverUsage) { + ownerCapsule.setLatestConsumeTimeForEnergy(now); + ownerCapsule.setEnergyUsage(ownerUsage); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + receiverCapsule.setLatestConsumeTimeForEnergy(now - 100); + receiverCapsule.setEnergyUsage(receiverUsage); + receiverCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + receiverCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + } +} 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..91559d86362 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -93,7 +93,14 @@ public void LogLoadTest() throws IOException { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { - LogService.load("non-existent.xml"); + // Empty path means no --log-config supplied: keep the classpath default. + LogService.load(""); + // Non-empty path pointing at a missing file must fail fast so operators + // don't silently run with the classpath default. + TronError missing = assertThrows(TronError.class, + () -> LogService.load("non-existent.xml")); + assertEquals(TronError.ErrCode.LOG_LOAD, missing.getErrCode()); + // Non-empty path pointing at an unparseable file must also surface. Path path = temporaryFolder.newFile("logback.xml").toPath(); TronError thrown = assertThrows(TronError.class, () -> LogService.load(path.toString())); assertEquals(TronError.ErrCode.LOG_LOAD, thrown.getErrCode()); @@ -139,8 +146,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/JsonRpcCallAndEstimateGasTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java new file mode 100644 index 00000000000..65defdab2ed --- /dev/null +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonRpcCallAndEstimateGasTest.java @@ -0,0 +1,277 @@ +package org.tron.core.jsonrpc; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.GrpcAPI.EstimateEnergyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.GrpcAPI.TransactionExtention; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.Manager; +import org.tron.core.exception.jsonrpc.JsonRpcInternalException; +import org.tron.core.services.NodeInfoService; +import org.tron.core.services.jsonrpc.TronJsonRpcImpl; +import org.tron.core.services.jsonrpc.types.CallArguments; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class JsonRpcCallAndEstimateGasTest { + + private static final String ERROR_REVERT_HEX = "08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"; + private static final String REVERT_MSG = "REVERT opcode executed"; + private static final String MOCK_FROM_ADDRESS = "0x0000000000000000000000000000000000000000"; + private static final String MOCK_TO_ADDRESS = "0x0000000000000000000000000000000000000001"; + + private enum EstimatePath { + CONSTANT_CALL, + ESTIMATE_ENERGY + } + + private final boolean originalEstimateEnergy = CommonParameter.getInstance().isEstimateEnergy(); + private TronJsonRpcImpl mockRpc; + + @After + public void tearDown() throws Exception { + if (mockRpc != null) { + mockRpc.close(); + mockRpc = null; + } + CommonParameter.getInstance().setEstimateEnergy(originalEstimateEnergy); + } + + @Test + public void testGetCallAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForPanicSelector() throws Exception { + byte[] panicData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + + mockRpc = newRpcWithMockedFailedCall(panicData, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testGetCallSkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.CONSTANT_CALL); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.getCall(newCallArgs(), "latest")); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasSkipsRevertReasonForEmptyData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[0], EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyAppendsRevertReason() throws Exception { + byte[] revertData = ByteArray.fromHexString(ERROR_REVERT_HEX); + + mockRpc = newRpcWithMockedFailedCall(revertData, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG + ": not enough input value", e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergySkipsRevertReasonForShortData() throws Exception { + mockRpc = newRpcWithMockedFailedCall(new byte[] {1, 2, 3}, EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + JsonRpcInternalException e = assertThrows(JsonRpcInternalException.class, + () -> mockRpc.estimateGas(newCallArgs())); + Assert.assertEquals(REVERT_MSG, e.getMessage()); + } + + @Test + public void testEstimateGasWithEstimateEnergyReturnsEstimatedEnergy() throws Exception { + long energyRequired = 0x4321L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyRequired, + EstimatePath.ESTIMATE_ENERGY); + CommonParameter.getInstance().setEstimateEnergy(true); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyRequired), result); + } + + @Test + public void testGetCallReturnsConstantResult() throws Exception { + byte[] part1 = ByteArray.fromHexString("deadbeef"); + byte[] part2 = ByteArray.fromHexString("cafebabe"); + + mockRpc = newRpcWithMockedSuccessfulCall(part1, part2); + + String result = mockRpc.getCall(newCallArgs(), "latest"); + + Assert.assertEquals("0xdeadbeefcafebabe", result); + } + + @Test + public void testEstimateGasReturnsEnergyUsed() throws Exception { + long energyUsed = 0x1234L; + + mockRpc = newRpcWithMockedEstimateGasSuccessfulCall(energyUsed, EstimatePath.CONSTANT_CALL); + CommonParameter.getInstance().setEstimateEnergy(false); + + String result = mockRpc.estimateGas(newCallArgs()); + + Assert.assertEquals(ByteArray.toJsonHex(energyUsed), result); + } + + private static CallArguments newCallArgs() { + CallArguments args = new CallArguments(); + args.setFrom(MOCK_FROM_ADDRESS); + args.setTo(MOCK_TO_ADDRESS); + args.setValue("0x0"); + args.setData("0x"); + return args; + } + + private static TronJsonRpcImpl newRpcWithMockedFailedCall(byte[] resData, EstimatePath path) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + estimateBuilder.setResult(retBuilder); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + Return.Builder retBuilder = invocation.getArgument(3); + extBuilder.addConstantResult(ByteString.copyFrom(resData)); + retBuilder.setMessage(ByteString.copyFromUtf8(REVERT_MSG)); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.FAILED)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedSuccessfulCall(byte[]... constantResults) + throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + for (byte[] bytes : constantResults) { + extBuilder.addConstantResult(ByteString.copyFrom(bytes)); + } + extBuilder.setEnergyUsed(0L); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } + + private static TronJsonRpcImpl newRpcWithMockedEstimateGasSuccessfulCall(long energyValue, + EstimatePath path) throws Exception { + Wallet mockWallet = mock(Wallet.class); + Manager mockManager = mock(Manager.class); + NodeInfoService mockNodeInfo = mock(NodeInfoService.class); + + when(mockWallet.createTransactionCapsule(any(), any())) + .thenReturn(new TransactionCapsule(Protocol.Transaction.newBuilder().build())); + when(mockWallet.getContract(any())).thenReturn(SmartContract.getDefaultInstance()); + + if (path == EstimatePath.ESTIMATE_ENERGY) { + when(mockWallet.estimateEnergy(any(), any(), any(), any(), any())) + .thenAnswer(invocation -> { + EstimateEnergyMessage.Builder estimateBuilder = invocation.getArgument(4); + estimateBuilder.setEnergyRequired(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } else { + when(mockWallet.triggerConstantContract(any(), any(), any(), any())) + .thenAnswer(invocation -> { + TransactionExtention.Builder extBuilder = invocation.getArgument(2); + extBuilder.setEnergyUsed(energyValue); + return Protocol.Transaction.newBuilder() + .addRet(Protocol.Transaction.Result.newBuilder() + .setRet(Protocol.Transaction.Result.code.SUCESS)) + .build(); + }); + } + + return new TronJsonRpcImpl(mockNodeInfo, mockWallet, mockManager); + } +} 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..e162ee917e9 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -1,20 +1,27 @@ package org.tron.core.jsonrpc; -import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.getByJsonBlockId; -import static org.tron.core.services.jsonrpc.TronJsonRpcImpl.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_PENDING_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.TAG_SAFE_SUPPORT_ERROR; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.isBlockTag; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockNumber; +import static org.tron.core.services.jsonrpc.JsonRpcApiUtil.parseBlockTag; import com.alibaba.fastjson.JSON; import com.google.gson.JsonArray; 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.HashMap; import java.util.List; import javax.annotation.Resource; 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 +32,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; @@ -45,6 +53,7 @@ import org.tron.core.services.interfaceJsonRpcOnSolidity.JsonRpcServiceOnSolidity; import org.tron.core.services.jsonrpc.FullNodeJsonRpcHttpService; import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.TronJsonRpc.LogFilterElement; import org.tron.core.services.jsonrpc.TronJsonRpcImpl; import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; import org.tron.core.services.jsonrpc.types.BlockResult; @@ -62,6 +71,8 @@ public class JsonrpcServiceTest extends BaseTest { private static final String OWNER_ADDRESS_ACCOUNT_NAME = "first"; private static final long LATEST_BLOCK_NUM = 10_000L; private static final long LATEST_SOLIDIFIED_BLOCK_NUM = 4L; + private static final String TAG_NOT_SUPPORT_ERROR = + "TAG [earliest | pending | finalized | safe] not supported"; private static TronJsonRpcImpl tronJsonRpc; @Resource @@ -109,11 +120,11 @@ public void init() { blockCapsule0 = BlockUtil.newGenesisBlockCapsule(); blockCapsule1 = new BlockCapsule(LATEST_BLOCK_NUM, Sha256Hash.wrap(ByteString.copyFrom( ByteArray.fromHexString( - "0304f784e4e7bae517bcab94c3e0c9214fb4ac7ff9d7d5a937d1f40031f87b81"))), 1, + "0304f784e4e7bae517bcab94c3e0c9214fb4ac7ff9d7d5a937d1f40031f87b81"))), 1000000, ByteString.copyFromUtf8("testAddress")); blockCapsule2 = new BlockCapsule(LATEST_SOLIDIFIED_BLOCK_NUM, Sha256Hash.wrap( ByteString.copyFrom(ByteArray.fromHexString( - "9938a342238077182498b464ac029222ae169360e540d1fd6aee7c2ae9575a06"))), 1, + "9938a342238077182498b464ac029222ae169360e540d1fd6aee7c2ae9575a06"))), 2000000, ByteString.copyFromUtf8("testAddress")); TransferContract transferContract1 = TransferContract.newBuilder().setAmount(1L) @@ -135,13 +146,15 @@ public void init() { transactionCapsule1 = new TransactionCapsule(transferContract1, ContractType.TransferContract); transactionCapsule1.setBlockNum(blockCapsule1.getNum()); + transactionCapsule1.setTimestamp(blockCapsule1.getTimeStamp()); TransactionCapsule transactionCapsule2 = new TransactionCapsule(transferContract2, ContractType.TransferContract); transactionCapsule2.setBlockNum(blockCapsule1.getNum()); + transactionCapsule2.setTimestamp(blockCapsule1.getTimeStamp()); TransactionCapsule transactionCapsule3 = new TransactionCapsule(transferContract3, ContractType.TransferContract); transactionCapsule3.setBlockNum(blockCapsule2.getNum()); - + transactionCapsule3.setTimestamp(blockCapsule2.getTimeStamp()); blockCapsule1.addTransaction(transactionCapsule1); blockCapsule1.addTransaction(transactionCapsule2); blockCapsule2.addTransaction(transactionCapsule3); @@ -181,6 +194,7 @@ public void init() { TransactionInfoCapsule transactionInfoCapsule = new TransactionInfoCapsule(); transactionInfoCapsule.setId(tx.getTransactionId().getBytes()); transactionInfoCapsule.setBlockNumber(blockCapsule1.getNum()); + transactionInfoCapsule.setBlockTimeStamp(blockCapsule1.getTimeStamp()); transactionInfoCapsule.addAllLog(logs); transactionRetCapsule1.addTransactionInfo(transactionInfoCapsule.getInstance()); }); @@ -192,6 +206,7 @@ public void init() { TransactionInfoCapsule transactionInfoCapsule = new TransactionInfoCapsule(); transactionInfoCapsule.setId(tx.getTransactionId().getBytes()); transactionInfoCapsule.setBlockNumber(blockCapsule2.getNum()); + transactionInfoCapsule.setBlockTimeStamp(blockCapsule2.getTimeStamp()); transactionRetCapsule2.addTransactionInfo(transactionInfoCapsule.getInstance()); }); dbManager.getTransactionRetStore() @@ -255,17 +270,13 @@ public void testGetBlockTransactionCountByNumber() { Assert.assertNull(result); } - try { - result = tronJsonRpc.ethGetBlockTransactionCountByNumber("pending"); - } catch (Exception e) { - Assert.assertEquals("TAG pending not supported", e.getMessage()); - } + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("pending")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); - try { - result = tronJsonRpc.ethGetBlockTransactionCountByNumber("qqqqq"); - } catch (Exception e) { - Assert.assertEquals("invalid block number", e.getMessage()); - } + Exception malformedEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("qqqqq")); + Assert.assertEquals("invalid block number", malformedEx.getMessage()); try { result = tronJsonRpc.ethGetBlockTransactionCountByNumber("latest"); @@ -282,6 +293,15 @@ public void testGetBlockTransactionCountByNumber() { } Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTransactions().size()), result); + // safe tag is not supported (new tag added in this refactor) + Exception safeEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("safe")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + + // hex that overflows long -> longValueExact rejects (previously silently truncated) + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockTransactionCountByNumber("0x10000000000000000")); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); } @Test @@ -345,20 +365,24 @@ 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()); + + // safe + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("safe", false)); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, e3.getMessage()); + + // hex overflows long -> longValueExact rejects + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.ethGetBlockByNumber("0x10000000000000000", false)); + Assert.assertEquals("invalid block number", e4.getMessage()); } @Test @@ -429,87 +453,90 @@ public void testServicesInit() { } @Test - 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()); - } - - try { - blkNum = getByJsonBlockId(null, wallet); + public void testBlockTagParsing() { + // isBlockTag + Assert.assertTrue(isBlockTag("pending")); + Assert.assertTrue(isBlockTag("latest")); + Assert.assertTrue(isBlockTag("earliest")); + Assert.assertTrue(isBlockTag("finalized")); + Assert.assertTrue(isBlockTag("safe")); + Assert.assertFalse(isBlockTag(null)); + Assert.assertFalse(isBlockTag("0xa")); + Assert.assertFalse(isBlockTag("")); + + // parseBlockTag: pending throws + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> parseBlockTag("pending", wallet)); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); + + // parseBlockTag: safe throws + Exception safeEx = Assert.assertThrows(Exception.class, + () -> parseBlockTag("safe", wallet)); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + + // parseBlockTag: latest -> headBlockNum + try { + long blkNum = parseBlockTag("latest", wallet); + Assert.assertEquals(LATEST_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: earliest -> 0 try { - blkNum = getByJsonBlockId("latest", wallet); + long blkNum = parseBlockTag("earliest", wallet); + Assert.assertEquals(0L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(-1, blkNum); + // parseBlockTag: finalized -> solidBlockNum try { - blkNum = getByJsonBlockId("finalized", wallet); + long blkNum = parseBlockTag("finalized", wallet); + Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(LATEST_SOLIDIFIED_BLOCK_NUM, blkNum); + // parseBlockNumber: hex -> number try { - blkNum = getByJsonBlockId("0xa", wallet); + long blkNum = parseBlockNumber("0xa", wallet); + Assert.assertEquals(10L, blkNum); } catch (Exception e) { Assert.fail(); } - Assert.assertEquals(10L, blkNum); - try { - getByJsonBlockId("abc", wallet); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("Incorrect hex syntax", e.getMessage()); - } + // parseBlockNumber: bad hex -> throws + Exception abcEx = Assert.assertThrows(Exception.class, + () -> parseBlockNumber("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\"")); - } + // parseBlockNumber: malformed hex -> throws + Exception hexEx = Assert.assertThrows(Exception.class, + () -> parseBlockNumber("0xxabc", wallet)); + // https://bugs.openjdk.org/browse/JDK-8176425, from JDK 12, the exception message is changed + Assert.assertTrue(hexEx.getMessage().startsWith("For input string: \"xabc\"")); } @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_NOT_SUPPORT_ERROR, 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_NOT_SUPPORT_ERROR, 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_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); try { balance = tronJsonRpc.getTrxBalance("0xabd4b9367799eaa3197fecb144eb71de1e049abc", @@ -522,83 +549,221 @@ public void testGetTrxBalance() { @Test public void testGetStorageAt() { - try { - tronJsonRpc.getStorageAt("", "", "earliest"); - Assert.fail("Expected to be thrown"); + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "earliest")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "pending")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "finalized")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "0x1")); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e5.getMessage()); + + // malformed hex -> BLOCK_NUM_ERROR + Exception e6 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getStorageAt("", "", "abc")); + Assert.assertEquals("invalid block number", e6.getMessage()); + + // latest happy path: address is an account, not a contract, so returns 32 zero bytes + try { + String value = tronJsonRpc.getStorageAt( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "0x0", "latest"); + Assert.assertEquals(ByteArray.toJsonHex(new byte[32]), value); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } + } - try { - tronJsonRpc.getStorageAt("", "", "pending"); - Assert.fail("Expected to be thrown"); + @Test + public void testGetABIOfSmartContract() { + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "earliest")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "pending")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "finalized")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + + // hex block number -> QUANTITY_NOT_SUPPORT_ERROR + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "0x1")); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e5.getMessage()); + + // malformed hex -> BLOCK_NUM_ERROR + Exception e6 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getABIOfSmartContract("", "abc")); + Assert.assertEquals("invalid block number", e6.getMessage()); + + // latest happy path: address is an account, not a contract, so returns "0x" + try { + String code = tronJsonRpc.getABIOfSmartContract( + "0xabd4b9367799eaa3197fecb144eb71de1e049abc", "latest"); + Assert.assertEquals("0x", code); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } + } - try { - tronJsonRpc.getStorageAt("", "", "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + @Test + public void testGetCall() { + Exception e1 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "earliest")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e1.getMessage()); + + Exception e2 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "pending")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e2.getMessage()); + + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "finalized")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, "safe")); + Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); } @Test - public void testGetABIOfSmartContract() { - try { - tronJsonRpc.getABIOfSmartContract("", "earliest"); - Assert.fail("Expected to be thrown"); + public void testGetTransactionByBlockNumberAndIndex() { + // valid hex block number: blockCapsule1 has 2 txs; index 0 is transactionCapsule1. + // Assert the returned tx actually resolves to transactionCapsule1's hash, + // block number, and index rather than just non-null. + try { + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals( + ByteArray.toJsonHex(transactionCapsule1.getTransactionId().getBytes()), + result.getHash()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); + Assert.assertEquals(ByteArray.toJsonHex(0L), result.getTransactionIndex()); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } + // index out of range in an existing block returns null try { - tronJsonRpc.getABIOfSmartContract("", "pending"); - Assert.fail("Expected to be thrown"); + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex( + ByteArray.toJsonHex(blockCapsule1.getNum()), "0x5"); + Assert.assertNull(result); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } + // latest -> blockCapsule1 (head) try { - tronJsonRpc.getABIOfSmartContract("", "finalized"); - Assert.fail("Expected to be thrown"); + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("latest", "0x0"); + Assert.assertNotNull(result); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), result.getBlockNumber()); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } - } - @Test - public void testGetCall() { + // finalized -> blockCapsule2 (solid), has 1 tx try { - tronJsonRpc.getCall(null, "earliest"); - Assert.fail("Expected to be thrown"); + TransactionResult result = + tronJsonRpc.getTransactionByBlockNumberAndIndex("finalized", "0x0"); + Assert.assertNotNull(result); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } + // non-existent block number returns null (not an error) try { - tronJsonRpc.getCall(null, "pending"); - Assert.fail("Expected to be thrown"); + TransactionResult result = tronJsonRpc.getTransactionByBlockNumberAndIndex("0x1", "0x0"); + Assert.assertNull(result); } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); + Assert.fail(); } - try { - tronJsonRpc.getCall(null, "finalized"); - Assert.fail("Expected to be thrown"); - } catch (Exception e) { - Assert.assertEquals("TAG [earliest | pending | finalized] not supported", - e.getMessage()); - } + // pending tag rejected + Exception pendingEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("pending", "0x0")); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, pendingEx.getMessage()); + + // safe tag rejected (new tag) + Exception safeEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("safe", "0x0")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeEx.getMessage()); + + // malformed hex rejected + Exception qqqEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("qqq", "0x0")); + Assert.assertEquals("invalid block number", qqqEx.getMessage()); + + // hex overflows long -> longValueExact rejects + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTransactionByBlockNumberAndIndex("0x10000000000000000", "0x0")); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); + } + + /** + * Tests the object-form second argument of eth_call: + * {"blockNumber": "0x..."} or {"blockHash": "0x..."}. + * Only the block-selector parsing is exercised here; the call() + * execution path is covered by other tests. + */ + @Test + public void testGetCallWithBlockObject() { + // neither HashMap nor String -> invalid json request + Exception nonMapEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, new Object())); + Assert.assertEquals("invalid json request", nonMapEx.getMessage()); + + // HashMap without blockNumber/blockHash keys -> invalid json request + Exception emptyMapEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, new HashMap())); + Assert.assertEquals("invalid json request", emptyMapEx.getMessage()); + + // blockNumber with malformed hex -> invalid block number + HashMap badHexParams = new HashMap<>(); + badHexParams.put("blockNumber", "xxx"); + Exception badHexEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, badHexParams)); + Assert.assertEquals("invalid block number", badHexEx.getMessage()); + + // blockNumber overflows long -> invalid block number (longValueExact) + HashMap overflowParams = new HashMap<>(); + overflowParams.put("blockNumber", "0x10000000000000000"); + Exception overflowEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, overflowParams)); + Assert.assertEquals("invalid block number", overflowEx.getMessage()); + + // blockNumber points to a non-existent block -> header not found + HashMap missingNumParams = new HashMap<>(); + missingNumParams.put("blockNumber", "0x1"); + Exception missingNumEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, missingNumParams)); + Assert.assertEquals("header not found", missingNumEx.getMessage()); + + // blockHash of an unknown block -> header for hash not found + HashMap missingHashParams = new HashMap<>(); + missingHashParams.put("blockHash", + "0x1111111111111111111111111111111111111111111111111111111111111111"); + Exception missingHashEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, missingHashParams)); + Assert.assertEquals("header for hash not found", missingHashEx.getMessage()); } /** @@ -666,13 +831,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 +854,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 +867,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 +881,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 +896,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 +919,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 +1019,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", @@ -969,6 +1120,30 @@ public void testMethodBlockRange() { } } + @Test + public void testGetLogs() { + try { + LogFilterElement[] logs = tronJsonRpc.getLogs( + new FilterRequest("0x2710", "0x2710", null, null, null)); + Assert.assertTrue(logs.length > 0); + LogFilterElement log = logs[0]; + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getNum()), log.getBlockNumber()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getBlockId().toString()), + log.getBlockHash()); + Assert.assertEquals("0x0", log.getLogIndex()); + Assert.assertFalse(log.isRemoved()); + Assert.assertEquals(1, log.getTopics().length); + Assert.assertEquals( + "0x0000000000000000000000000000000000000000000000000000746f70696331", + log.getTopics()[0]); + Assert.assertEquals(ByteArray.toJsonHex("data1".getBytes()), log.getData()); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), + log.getBlockTimestamp()); + } catch (Exception e) { + Assert.fail(); + } + } + @Test public void testNewFilterFinalizedBlock() { @@ -978,39 +1153,109 @@ 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"); + Exception e3 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "latest", null, null, null))); + Assert.assertEquals("invalid block range params", e3.getMessage()); + + Exception e4 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e4.getMessage()); + + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null))); + Assert.assertEquals("invalid block range params", e5.getMessage()); + } + + /** + * Tag handling at the RPC boundary for eth_newFilter / eth_getLogs / eth_getFilterLogs. + * - safe/pending are rejected inside LogFilterWrapper (parseBlockNumber -> parseBlockTag) + * - finalized is intercepted by newFilter's upfront guard, but allowed by getLogs + * - getFilterLogs round-trips a filter created with concrete block numbers + */ + @Test + public void testLogFilterTagHandling() { + // eth_newFilter: safe in fromBlock -> TAG_SAFE_SUPPORT_ERROR + Exception newFilterSafeFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("safe", null, null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, newFilterSafeFromEx.getMessage()); + + // eth_newFilter: safe in toBlock + Exception newFilterSafeToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "safe", null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, newFilterSafeToEx.getMessage()); + + // eth_newFilter: pending in fromBlock + Exception newFilterPendingFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("pending", null, null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, newFilterPendingFromEx.getMessage()); + + // eth_newFilter: pending in toBlock + Exception newFilterPendingToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.newFilter(new FilterRequest("0x1", "pending", null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, newFilterPendingToEx.getMessage()); + + // eth_getLogs: safe in fromBlock + Exception getLogsSafeFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest("safe", null, null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, getLogsSafeFromEx.getMessage()); + + // eth_getLogs: safe in toBlock + Exception getLogsSafeToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest(null, "safe", null, null, null))); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, getLogsSafeToEx.getMessage()); + + // eth_getLogs: pending in fromBlock + Exception getLogsPendingFromEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest("pending", null, null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, getLogsPendingFromEx.getMessage()); + + // eth_getLogs: pending in toBlock + Exception getLogsPendingToEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getLogs(new FilterRequest(null, "pending", null, null, null))); + Assert.assertEquals(TAG_PENDING_SUPPORT_ERROR, getLogsPendingToEx.getMessage()); + + // eth_getLogs: finalized is accepted (resolves to solidBlockNum via parseBlockTag). + // With fromBlock empty, Strategy 2 resolves the range to [solid, solid]. blockCapsule2 + // (solid=4) has no logs in test fixtures, so result must be empty. + try { + LogFilterElement[] result = + tronJsonRpc.getLogs(new FilterRequest(null, "finalized", null, null, null)); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.length); } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); + Assert.fail(e.getMessage()); } + // End-to-end happy path for eth_getLogs and eth_getFilterLogs. + // Query range [head, head] = [blockCapsule1, blockCapsule1]. No address/topic filter, + // so LogBlockQuery marks all blocks in the range as candidates. LogMatch then iterates + // blockCapsule1's 2 txs * 2 logs each = 4 LogFilterElements. + String headHex = ByteArray.toJsonHex(blockCapsule1.getNum()); + int expectedLogs = blockCapsule1.getTransactions().size() * 2; + try { - tronJsonRpc.newFilter(new FilterRequest("0x1", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); + LogFilterElement[] directResult = + tronJsonRpc.getLogs(new FilterRequest(headHex, headHex, null, null, null)); + Assert.assertEquals(expectedLogs, directResult.length); } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); + Assert.fail(e.getMessage()); } try { - tronJsonRpc.newFilter(new FilterRequest("finalized", "finalized", null, null, null)); - Assert.fail("Expected to be thrown"); + String filterIdHex = tronJsonRpc.newFilter( + new FilterRequest(headHex, headHex, null, null, null)); + LogFilterElement[] filterResult = tronJsonRpc.getFilterLogs(filterIdHex); + Assert.assertEquals(expectedLogs, filterResult.length); } catch (Exception e) { - Assert.assertEquals("invalid block range params", e.getMessage()); + Assert.fail(e.getMessage()); } } @@ -1026,6 +1271,10 @@ public void testGetBlockReceipts() { Assert.assertEquals( JSON.toJSONString(transactionReceipt), JSON.toJSONString(transactionReceipt1)); + + Assert.assertTrue(transactionReceipt1.getLogs().length > 0); + Assert.assertEquals(ByteArray.toJsonHex(blockCapsule1.getTimeStamp() / 1000), + transactionReceipt1.getLogs()[0].getBlockTimestamp()); } } catch (JsonRpcInvalidParamsException | JsonRpcInternalException e) { throw new RuntimeException(e); @@ -1052,19 +1301,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"); @@ -1087,6 +1330,13 @@ public void testGetBlockReceipts() { throw new RuntimeException(e); } + Exception safeReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("safe")); + Assert.assertEquals(TAG_SAFE_SUPPORT_ERROR, safeReceiptsEx.getMessage()); + + Exception overflowReceiptsEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getBlockReceipts("0x10000000000000000")); + Assert.assertEquals("invalid block number", overflowReceiptsEx.getMessage()); } @Test @@ -1099,4 +1349,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/LogMatchExactlyTest.java b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java index 0f9f125b74e..2151801fc59 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/LogMatchExactlyTest.java @@ -37,6 +37,7 @@ private TransactionInfo createTransactionInfo(byte[] address, byte[][] topicArra LogInfo logInfo = new LogInfo(address, topics, data); logList.add(LogInfo.buildLog(logInfo)); builder.addAllLog(logList); + builder.setBlockTimeStamp(1000000L); return builder.build(); } @@ -230,6 +231,8 @@ public void testMatchBlock() { LogFilterElement logFilterElement1 = elementList.get(0); LogFilterElement logFilterElement2 = elementList2.get(0); + Assert.assertEquals("0x3e8", logFilterElement1.getBlockTimestamp()); + Assert.assertEquals("0x3e8", logFilterElement2.getBlockTimestamp()); Assert.assertEquals(logFilterElement1.hashCode(), logFilterElement2.hashCode()); Assert.assertEquals(logFilterElement1, logFilterElement2); 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/metrics/prometheus/PrometheusApiServiceTest.java b/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java index d4d758b7a98..dd260a1b869 100644 --- a/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java +++ b/framework/src/test/java/org/tron/core/metrics/prometheus/PrometheusApiServiceTest.java @@ -7,6 +7,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -25,6 +26,7 @@ import org.tron.common.utils.ByteArray; import org.tron.common.utils.PublicMethod; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.StringUtil; import org.tron.common.utils.Utils; import org.tron.consensus.dpos.DposSlot; import org.tron.core.ChainBaseManager; @@ -38,6 +40,8 @@ @Slf4j(topic = "metric") public class PrometheusApiServiceTest extends BaseTest { + + static LocalDateTime localDateTime = LocalDateTime.now(); @Resource private DposSlot dposSlot; @@ -65,7 +69,7 @@ protected static void initParameter(CommonParameter parameter) { parameter.setMetricsPrometheusEnable(true); } - protected void check() throws Exception { + protected void check(byte[] address, Map witnessAndAccount) throws Exception { Double memoryBytes = CollectorRegistry.defaultRegistry.getSampleValue( "system_total_physical_memory_bytes"); Assert.assertNotNull(memoryBytes); @@ -80,6 +84,32 @@ protected void check() throws Exception { new String[] {"sync"}, new String[] {"false"}); Assert.assertNotNull(pushBlock); Assert.assertEquals(pushBlock.intValue(), blocks + 1); + + String minerBase58 = StringUtil.encode58Check(address); + // Query histogram bucket le="0.0" for empty blocks + Double emptyBlock = CollectorRegistry.defaultRegistry.getSampleValue( + "tron:block_transaction_count_bucket", + new String[] {MetricLabels.Histogram.MINER, "le"}, new String[] {minerBase58, "0.0"}); + + Assert.assertNotNull("Empty block bucket should exist for miner: " + minerBase58, emptyBlock); + Assert.assertEquals("Should have 1 empty block", 1, emptyBlock.intValue()); + + // Collect empty blocks for each new witness in witnessAndAccount (excluding initial address) + ByteString addressByteString = ByteString.copyFrom(address); + int totalNewWitnessEmptyBlocks = 0; + for (ByteString witnessAddress : witnessAndAccount.keySet()) { + if (witnessAddress.equals(addressByteString)) { + continue; + } + String witnessBase58 = StringUtil.encode58Check(witnessAddress.toByteArray()); + int witnessEmptyBlock = CollectorRegistry.defaultRegistry.getSampleValue( + "tron:block_transaction_count_bucket", + new String[] {MetricLabels.Histogram.MINER, "le"}, new String[] {witnessBase58, "0.0"}) + .intValue(); + totalNewWitnessEmptyBlocks += witnessEmptyBlock; + } + Assert.assertEquals(blocks, totalNewWitnessEmptyBlocks); + Double errorLogs = CollectorRegistry.defaultRegistry.getSampleValue( "tron:error_info_total", new String[] {"net"}, new String[] {MetricLabels.UNDEFINED}); Assert.assertNull(errorLogs); @@ -130,10 +160,16 @@ public void testMetric() throws Exception { Map witnessAndAccount = addTestWitnessAndAccount(); witnessAndAccount.put(ByteString.copyFrom(address), key); + + // Schedule the new witnesses (excluding initial address) so dposSlot rotates blocks among them + List newActiveWitnesses = new ArrayList<>(witnessAndAccount.keySet()); + newActiveWitnesses.remove(ByteString.copyFrom(address)); + chainBaseManager.getWitnessScheduleStore().saveActiveWitnesses(newActiveWitnesses); + for (int i = 0; i < blocks; i++) { generateBlock(witnessAndAccount); } - check(); + check(address, witnessAndAccount); } private Map addTestWitnessAndAccount() { 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/P2pEventHandlerImplTest.java b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java index 03c79f495ee..2e79bbf5809 100644 --- a/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java +++ b/framework/src/test/java/org/tron/core/net/P2pEventHandlerImplTest.java @@ -34,6 +34,7 @@ public static void init() throws Exception { public void testProcessInventoryMessage() throws Exception { CommonParameter parameter = CommonParameter.getInstance(); parameter.setMaxTps(10); + parameter.setMaxBlockInvPerSecond(10); PeerStatistics peerStatistics = new PeerStatistics(); @@ -75,7 +76,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10); - Assert.assertEquals(110, count); + Assert.assertEquals(10, count); // 100 hashes dropped: 10+100=110 > maxCountIn10s(100) list.clear(); for (int i = 0; i < 100; i++) { @@ -88,7 +89,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10); - Assert.assertEquals(110, count); + Assert.assertEquals(10, count); // still dropped: window=10, 10+100=110 > 100 list.clear(); for (int i = 0; i < 200; i++) { @@ -101,7 +102,7 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10); - Assert.assertEquals(200, count); + Assert.assertEquals(0, count); // 200 hashes dropped: 0+200=200 > maxBlockInvIn10s(100) list.clear(); for (int i = 0; i < 100; i++) { @@ -114,10 +115,100 @@ public void testProcessInventoryMessage() throws Exception { count = peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10); - Assert.assertEquals(300, count); + Assert.assertEquals(100, count); // passes: window=0, 0+100=100, not > 100 } + @Test + public void testCheckInvRateLimitTrxBoundary() throws Exception { + // maxTps=10 → maxCountIn10s=100 + CommonParameter parameter = CommonParameter.getInstance(); + parameter.setMaxTps(10); + parameter.setMaxBlockInvPerSecond(10); + + PeerStatistics peerStatistics = new PeerStatistics(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getPeerStatistics()).thenReturn(peerStatistics); + + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + Method method = handler.getClass() + .getDeclaredMethod("processMessage", PeerConnection.class, byte[].class); + method.setAccessible(true); + + // Fill window to 91: send 91 TRX hashes → passes (0+91=91 ≤ 100) + List list91 = new ArrayList<>(); + for (int i = 0; i < 91; i++) { + list91.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msg91 = new InventoryMessage(list91, InventoryType.TRX); + method.invoke(handler, peer, msg91.getSendBytes()); + Assert.assertEquals(91, + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + + // Send 9 more TRX hashes → passes (91+9=100, not > 100) + List list9 = new ArrayList<>(); + for (int i = 0; i < 9; i++) { + list9.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msg9 = new InventoryMessage(list9, InventoryType.TRX); + method.invoke(handler, peer, msg9.getSendBytes()); + Assert.assertEquals(100, + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + + // Send 1 more TRX hash → DROPPED (100+1=101 > 100) + List list1 = new ArrayList<>(); + list1.add(new Sha256Hash(0, new byte[32])); + InventoryMessage msg1 = new InventoryMessage(list1, InventoryType.TRX); + method.invoke(handler, peer, msg1.getSendBytes()); + Assert.assertEquals(100, // count unchanged: message was dropped + peer.getPeerStatistics().messageStatistics.tronInTrxInventoryElement.getCount(10)); + } + + @Test + public void testCheckInvRateLimitBlockBoundary() throws Exception { + // maxBlockInvPerSecond=10 → maxBlockInvIn10s=100 + CommonParameter parameter = CommonParameter.getInstance(); + parameter.setMaxTps(1000); + parameter.setMaxBlockInvPerSecond(10); + + PeerStatistics peerStatistics = new PeerStatistics(); + PeerConnection peer = mock(PeerConnection.class); + Mockito.when(peer.getPeerStatistics()).thenReturn(peerStatistics); + + P2pEventHandlerImpl handler = new P2pEventHandlerImpl(); + Method method = handler.getClass() + .getDeclaredMethod("processMessage", PeerConnection.class, byte[].class); + method.setAccessible(true); + + // Send 101 BLOCK hashes → DROPPED (0+101=101 > 100) + List list101 = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + list101.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msgBlock101 = new InventoryMessage(list101, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock101.getSendBytes()); + Assert.assertEquals(0, + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + + // Send 100 BLOCK hashes → passes (0+100=100, not > 100) + List list100 = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + list100.add(new Sha256Hash(i, new byte[32])); + } + InventoryMessage msgBlock100 = new InventoryMessage(list100, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock100.getSendBytes()); + Assert.assertEquals(100, + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + + // Send 1 more BLOCK hash → DROPPED (100+1=101 > 100) + List list1 = new ArrayList<>(); + list1.add(new Sha256Hash(0, new byte[32])); + InventoryMessage msgBlock1 = new InventoryMessage(list1, InventoryType.BLOCK); + method.invoke(handler, peer, msgBlock1.getSendBytes()); + Assert.assertEquals(100, // count unchanged: message was dropped + peer.getPeerStatistics().messageStatistics.tronInBlockInventoryElement.getCount(10)); + } + @Test public void testUpdateLastInteractiveTime() throws Exception { PeerConnection peer = new PeerConnection(); 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..e87b2566205 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; @@ -150,6 +149,7 @@ public class RpcApiServicesTest { public static void init() throws IOException { Args.setParam(new String[] {"-d", temporaryFolder.newFolder().toString()}, TestConstants.TEST_CONF); + getInstance().allowShieldedTransactionApi = true; Assert.assertEquals(5, getInstance().getRpcMaxRstStream()); Assert.assertEquals(10, getInstance().getRpcSecondsPerWindow()); String OWNER_ADDRESS = Wallet.getAddressPreFixString() @@ -188,7 +188,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 +204,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 +217,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/CreateSpendAuthSigServletTest.java b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java index 85d6764132b..d3ebf26a261 100644 --- a/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java @@ -8,7 +8,9 @@ import java.io.UnsupportedEncodingException; import javax.annotation.Resource; import org.apache.http.client.methods.HttpPost; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -18,6 +20,8 @@ public class CreateSpendAuthSigServletTest extends BaseTest { + private static boolean origShieldedApi; + static { Args.setParam( new String[]{ @@ -26,6 +30,17 @@ public class CreateSpendAuthSigServletTest extends BaseTest { ); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + @Resource private CreateSpendAuthSigServlet createSpendAuthSigServlet; diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java new file mode 100644 index 00000000000..77ea73999d1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatInt64AsStringTest.java @@ -0,0 +1,264 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import com.google.protobuf.UInt64Value; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; +import org.tron.protos.Protocol; + +/** + * Tests for {@link JsonFormat#setInt64AsString(boolean)} / + * {@link JsonFormat#clearInt64AsString()} / {@link JsonFormat#isInt64AsString()}. + * + *

Tron protos do not define uint64/fixed64 fields directly; all 64-bit values use int64. + * The uint64 branch is exercised using {@link com.google.protobuf.UInt64Value}, a protobuf + * well-known wrapper with a single {@code uint64 value} field. + */ +public class JsonFormatInt64AsStringTest { + + /** Defensive cleanup in case a test leaves the ThreadLocal dirty. */ + @After + public void clearState() { + JsonFormat.clearInt64AsString(); + } + + @Test + public void defaultBehaviorUnchangedWhenUnset() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + String out = JsonFormat.printToString(account, true); + assertTrue("expected unquoted balance, got: " + out, + out.contains("\"balance\":123456789012345") + || out.contains("\"balance\": 123456789012345")); + assertFalse("balance should not be quoted by default, got: " + out, + out.contains("\"balance\":\"123456789012345\"") + || out.contains("\"balance\": \"123456789012345\"")); + } + + @Test + public void int64FieldQuotedWhenSet() { + Protocol.Account account = Protocol.Account.newBuilder() + .setBalance(123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("expected quoted balance, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64FieldQuotedWhenSet() { + UInt64Value v = UInt64Value.of(9007199254740993L); // 2^53 + 1 + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(v, true); + assertTrue("expected quoted uint64 value, got: " + out, + out.contains("\"9007199254740993\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void uint64DefaultUnquoted() { + UInt64Value v = UInt64Value.of(9007199254740993L); + String out = JsonFormat.printToString(v, true); + assertTrue("expected unquoted uint64 value, got: " + out, + out.contains("9007199254740993")); + assertFalse("uint64 should not be quoted by default, got: " + out, + out.contains("\"9007199254740993\"")); + } + + @Test + public void stringBytesEnumNotAffected() { + // Note: proto3 does not serialize default-valued fields, so enum/bytes fields are + // set to non-default values to verify they appear in the output. + Protocol.Account account = Protocol.Account.newBuilder() + .setAccountName(ByteString.copyFromUtf8("alice")) + .setType(Protocol.AccountType.AssetIssue) // non-default enum value + .setBalance(1L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + // balance int64 should be quoted + assertTrue("balance should be quoted, got: " + out, out.contains("\"1\"")); + // enum type serialized by name (not a number), not affected by int64_as_string + assertTrue("enum type should appear as name, got: " + out, + out.contains("AssetIssue")); + // bytes account_name should still serialize normally + assertTrue("account_name should appear, got: " + out, out.contains("account_name")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void nestedInt64FieldsQuoted() { + Protocol.Block block = Protocol.Block.newBuilder() + .setBlockHeader(Protocol.BlockHeader.newBuilder() + .setRawData(Protocol.BlockHeader.raw.newBuilder() + .setNumber(9007199254740993L) // 2^53 + 1 + .setTimestamp(1700000000000L) + .build()) + .build()) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(block, true); + assertTrue("nested number should be quoted, got: " + out, + out.contains("\"9007199254740993\"")); + assertTrue("nested timestamp should be quoted, got: " + out, + out.contains("\"1700000000000\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void mapStringInt64ValuesQuoted() { + Protocol.Account account = Protocol.Account.newBuilder() + .putAsset("USDT", 123456789012345L) + .build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("map value should be quoted, got: " + out, + out.contains("\"123456789012345\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + + @Test + public void boundaryValuesAllQuoted() { + // Note: proto3 does not serialize a field whose value equals its type default (0 for int64), + // so 0L is covered separately via defaultBehaviorUnchangedWhenUnset / uint64DefaultUnquoted + // (both use non-default values) and does not need an explicit quoted-output test. + long[] values = { + (1L << 53) - 1, // max safe JS integer + 1L << 53, // boundary + (1L << 53) + 1, // first unsafe + Long.MAX_VALUE, + Long.MIN_VALUE, + -1L + }; + for (long v : values) { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(v).build(); + JsonFormat.setInt64AsString(true); + try { + String out = JsonFormat.printToString(account, true); + assertTrue("value=" + v + " expected quoted, got: " + out, + out.contains("\"" + v + "\"")); + } finally { + JsonFormat.clearInt64AsString(); + } + } + } + + @Test + public void clearResetsState() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + JsonFormat.clearInt64AsString(); + String out = JsonFormat.printToString(account, true); + assertFalse("state should be cleared, got: " + out, out.contains("\"1\"")); + } + + @Test + public void clearInFinallySurvivesException() { + Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + JsonFormat.setInt64AsString(true); + try { + throw new RuntimeException("boom"); + } catch (RuntimeException expected) { + // expected + } finally { + JsonFormat.clearInt64AsString(); + } + String out = JsonFormat.printToString(account, true); + assertFalse("state leaked after exception, got: " + out, out.contains("\"1\"")); + } + + @Test + public void isInt64AsStringReflectsCurrentState() { + assertFalse(JsonFormat.isInt64AsString()); + JsonFormat.setInt64AsString(true); + try { + assertTrue(JsonFormat.isInt64AsString()); + } finally { + JsonFormat.clearInt64AsString(); + } + assertFalse(JsonFormat.isInt64AsString()); + } + + @Test + public void threadIsolation() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + final CountDownLatch barrier = new CountDownLatch(2); + ExecutorService ex = Executors.newFixedThreadPool(2); + try { + Future trueThread = ex.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + Future falseThread = ex.submit(() -> { + barrier.countDown(); + barrier.await(); + return JsonFormat.printToString(account, true); + }); + String withSet = trueThread.get(5, TimeUnit.SECONDS); + String noSet = falseThread.get(5, TimeUnit.SECONDS); + assertTrue("trueThread should see quoted: " + withSet, + withSet.contains("\"1\"")); + assertFalse("falseThread should see unquoted: " + noSet, + noSet.contains("\"1\"")); + } finally { + ex.shutdownNow(); + } + } + + @Test + public void noPollutionOnThreadReuse() throws Exception { + final Protocol.Account account = Protocol.Account.newBuilder().setBalance(1L).build(); + ExecutorService single = Executors.newSingleThreadExecutor(); + try { + Future firstRun = single.submit(() -> { + JsonFormat.setInt64AsString(true); + try { + return JsonFormat.printToString(account, true); + } finally { + JsonFormat.clearInt64AsString(); + } + }); + assertTrue(firstRun.get(5, TimeUnit.SECONDS).contains("\"1\"")); + + // Reuse the same thread; without a new set, state must be cleared. + Future secondRun = single.submit(() -> JsonFormat.printToString(account, true)); + String second = secondRun.get(5, TimeUnit.SECONDS); + assertFalse("thread reuse leaked quoted state: " + second, + second.contains("\"1\"")); + } finally { + single.shutdownNow(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java new file mode 100644 index 00000000000..882c5f99833 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletInt64Test.java @@ -0,0 +1,164 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; +import javax.annotation.Resource; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; + +/** + * End-to-end integration tests for {@link RateLimiterServlet#service} wiring of the + * {@code int64_as_string} flag. The single-class {@link JsonFormatInt64AsStringTest} verifies + * the {@code JsonFormat} ThreadLocal mechanism in isolation; this test verifies the full + * request-handling chain: URL query --> {@code service()} --> ThreadLocal --> output, and + * the {@code finally} clear that prevents state leakage across reused threads. + * + *

Pins four contracts: + *

    + *
  1. GET with {@code ?int64_as_string=true} produces quoted int64 fields.
  2. + *
  3. GET without the flag produces unquoted int64 fields (regression baseline).
  4. + *
  5. POST never honors the flag, regardless of source -- GET-only is the documented + * contract under issue #6568.
  6. + *
  7. {@code service()}'s {@code finally} block clears the ThreadLocal so reused Tomcat + * threads do not leak state between requests.
  8. + *
+ * + *

Uses {@link GetNowBlockServlet} as the fixture servlet because its response goes through + * {@code JsonFormat.printToString}, which is what the ThreadLocal actually controls. + */ +public class RateLimiterServletInt64Test extends BaseTest { + + @Resource(name = "getNowBlockServlet") + private GetNowBlockServlet servlet; + + @Resource(name = "getBurnTrxServlet") + private GetBurnTrxServlet handBuiltServlet; + + static { + Args.setParam( + new String[]{ + "--output-directory", dbPath(), + }, TestConstants.TEST_CONF + ); + } + + @Before + public void clearBefore() { + JsonFormat.clearInt64AsString(); + } + + @After + public void clearAfter() { + JsonFormat.clearInt64AsString(); + } + + /** Contract 1: GET with int64_as_string=true on URL query produces quoted int64 fields. */ + @Test + public void getWithUrlFlagQuotesInt64() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** Contract 2: GET without flag produces unquoted int64 fields (default behavior). */ + @Test + public void getWithoutFlagKeepsUnquoted() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertTrue("timestamp should be unquoted when no flag, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\\d+.*")); + } + } + + /** + * Contract 3: POST never honors int64_as_string, regardless of where the flag is placed. + * Pins the GET-only design contract for issue #6568. Any future PR that tries to extend + * support to POST will fail this test, forcing an explicit design review. + */ + @Test + public void postWithUrlFlagIgnored() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + String body = readBody(response); + if (body.contains("\"timestamp\"")) { + assertFalse("POST URL flag must be ignored under GET-only design, got: " + body, + body.matches("(?s).*\"timestamp\"\\s*:\\s*\"\\d+\".*")); + } + } + + /** + * Contract 4 (CRITICAL): service() must clear the ThreadLocal in finally. Without this + * clear, reused Tomcat threads leak the flag across requests, producing intermittent + * quoted/unquoted output that is extremely hard to debug in production. + */ + @Test + public void serviceClearsThreadLocalInFinally() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + servlet.service(request, new MockHttpServletResponse()); + assertFalse( + "RateLimiterServlet.service must clear int64_as_string ThreadLocal in its finally " + + "block. Removing this clear will leak state across requests on reused threads.", + JsonFormat.isInt64AsString()); + } + + /** + * Contract 5: hand-built JSON servlets (the ones that emit JSON literals manually instead + * of going through {@link JsonFormat#printToString}) honor the flag. The previous tests use + * {@link GetNowBlockServlet} which goes through {@code printToString}; this test uses + * {@link GetBurnTrxServlet} as a representative of the four ternary-style servlets + * (GetBurnTrx / GetPendingSize / GetTransactionCountByBlockNum / GetReward) to lock down + * their {@code isInt64AsString() ? quoted : unquoted} branch -- so a future refactor that + * inverts the ternary or breaks the quote placement fails visibly here. + */ + @Test + public void handBuiltJsonServletQuotesInt64WhenFlagSet() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("int64_as_string", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be quoted when int64_as_string=true, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\"\\d+\".*")); + } + + /** Contract 6: hand-built JSON servlets default to unquoted output (regression baseline). */ + @Test + public void handBuiltJsonServletKeepsUnquotedByDefault() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + handBuiltServlet.service(request, response); + String body = readBody(response); + assertTrue("burnTrxAmount should be unquoted by default, got: " + body, + body.matches("(?s).*\"burnTrxAmount\"\\s*:\\s*\\d+.*")); + } + + private String readBody(MockHttpServletResponse response) throws UnsupportedEncodingException { + return response.getContentAsString(); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java new file mode 100644 index 00000000000..4ae76f85dfb --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletTest.java @@ -0,0 +1,93 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Verifies RateLimiterServlet's adapter resolution: strict whitelist + * (no Class.forName arbitrary class loading), fail-fast on unknown or + * empty names, and successful construction of every whitelisted adapter. + */ +public class RateLimiterServletTest { + + private static final Map> allowedAdapters = + RateLimiterServlet.ALLOWED_ADAPTERS; + + @Test + public void testWhitelistContents() { + assertEquals(GlobalPreemptibleAdapter.class, + allowedAdapters.get(GlobalPreemptibleAdapter.class.getSimpleName())); + assertEquals(QpsRateLimiterAdapter.class, + allowedAdapters.get(QpsRateLimiterAdapter.class.getSimpleName())); + assertEquals(IPQPSRateLimiterAdapter.class, + allowedAdapters.get(IPQPSRateLimiterAdapter.class.getSimpleName())); + assertEquals(DefaultBaseQqsAdapter.class, + allowedAdapters.get(DefaultBaseQqsAdapter.class.getSimpleName())); + } + + @Test + public void testWhitelistRejectsUnknownAdapter() { + assertNull(allowedAdapters.get("EvilAdapter")); + assertNull(allowedAdapters.get("java.lang.Runtime")); + } + + @Test + public void testUnknownAdapterThrowsTronError() { + // Fail-fast parity with the pre-whitelist Class.forName behavior: an unknown + // adapter name raises TronError from @PostConstruct so Spring startup aborts + // rather than silently masking a misconfigured node. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("UnknownAdapter", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + assertTrue(e.getMessage().contains("UnknownAdapter")); + assertTrue(e.getMessage().contains("TestServlet")); + } + + @Test + public void testDefaultAdapterNameBuildsDefaultBaseQqsAdapter() { + // When no config entry exists for a servlet, addRateContainer passes + // DEFAULT_ADAPTER_NAME to buildAdapter; verify it resolves to + // DefaultBaseQqsAdapter. + IRateLimiter limiter = RateLimiterServlet.buildAdapter( + RateLimiterServlet.DEFAULT_ADAPTER_NAME, "qps=100", "TestServlet"); + assertNotNull(limiter); + assertTrue(limiter instanceof DefaultBaseQqsAdapter); + } + + @Test + public void testEmptyAdapterNameThrowsTronError() { + // Fail-fast parity with original: a configured-but-empty strategy name is + // a configuration bug and must not be silently replaced by the default. + TronError e = assertThrows(TronError.class, + () -> RateLimiterServlet.buildAdapter("", "qps=100", "TestServlet")); + assertEquals(TronError.ErrCode.RATE_LIMITER_INIT, e.getErrCode()); + } + + @Test + public void testBuildsEachWhitelistedAdapter() { + // Exercises the newInstance(String) constructor path for every whitelisted + // adapter so a signature/strategy-class break on any entry fails here + // instead of at node startup. + assertTrue(RateLimiterServlet.buildAdapter( + QpsRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof QpsRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + IPQPSRateLimiterAdapter.class.getSimpleName(), "qps=100", "TestServlet") + instanceof IPQPSRateLimiterAdapter); + assertTrue(RateLimiterServlet.buildAdapter( + GlobalPreemptibleAdapter.class.getSimpleName(), "permit=1", "TestServlet") + instanceof GlobalPreemptibleAdapter); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java index 221c5a7a165..7c05e0e9cfe 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilMockTest.java @@ -1,11 +1,15 @@ package org.tron.core.services.http; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; import java.security.InvalidParameterException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.junit.After; @@ -44,7 +48,7 @@ public void testPrintTransactionFee() { public void testPrintBlockList() { BlockCapsule blockCapsule1 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); - BlockCapsule blockCapsule2 = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + BlockCapsule blockCapsule2 = new BlockCapsule(2, Sha256Hash.ZERO_HASH, System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); GrpcAPI.BlockList list = GrpcAPI.BlockList.newBuilder() .addBlock(blockCapsule1.getInstance()) @@ -52,6 +56,148 @@ public void testPrintBlockList() { .build(); String out = Util.printBlockList(list, true); Assert.assertNotNull(out); + + JSONObject json = JSONObject.parseObject(out); + Assert.assertTrue(json.containsKey("block")); + JSONArray blockArray = json.getJSONArray("block"); + Assert.assertEquals(2, blockArray.size()); + + // verify each block has correct structure + for (int i = 0; i < blockArray.size(); i++) { + JSONObject blockJson = blockArray.getJSONObject(i); + Assert.assertTrue(blockJson.containsKey("blockID")); + Assert.assertTrue(blockJson.containsKey("block_header")); + Assert.assertFalse(blockJson.getString("blockID").isEmpty()); + JSONObject blockHeader = blockJson.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONEmptyTransactions() { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), true); + Assert.assertTrue(json.containsKey("blockID")); + Assert.assertTrue(json.containsKey("block_header")); + Assert.assertFalse(json.containsKey("transactions")); + Assert.assertFalse(json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(blockHeader); + Assert.assertTrue(blockHeader.containsKey("raw_data")); + } + + @Test + public void testPrintBlockToJSONWithTransactions() { + // Structural invariants must hold under either visible flag; the flag-driven + // encoding difference is covered by testPrintBlockToJSONVisibleFlagAffectsAddressEncoding. + for (boolean visible : new boolean[]{true, false}) { + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + blockCapsule.addTransaction(getTransactionCapsuleExample()); + + JSONObject json = Util.printBlockToJSON(blockCapsule.getInstance(), visible); + + String msg = "visible=" + visible; + Assert.assertTrue(msg, json.containsKey("blockID")); + Assert.assertTrue(msg, json.containsKey("block_header")); + Assert.assertTrue(msg, json.containsKey("transactions")); + Assert.assertFalse(msg, json.getString("blockID").isEmpty()); + JSONObject blockHeader = json.getJSONObject("block_header"); + Assert.assertNotNull(msg, blockHeader); + Assert.assertTrue(msg, blockHeader.containsKey("raw_data")); + + JSONArray txArray = json.getJSONArray("transactions"); + Assert.assertEquals(msg, 1, txArray.size()); + JSONObject txJson = txArray.getJSONObject(0); + Assert.assertTrue(msg, txJson.containsKey("txID")); + Assert.assertTrue(msg, txJson.containsKey("raw_data")); + } + } + + @Test + public void testPrintBlockToJSONVisibleFlagAffectsAddressEncoding() { + // Pins the optimized printBlockToJSON against the prior behavior: the + // visible flag must still thread through to JsonFormat so address-bearing + // fields switch encoding while byte-identity fields stay stable. + ByteString witnessAddress = ByteString.copyFrom( + ByteArray.fromHexString("41548794500882809695a8a687866e76d4271a1abc")); + BlockCapsule blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), witnessAddress); + + JSONObject visible = Util.printBlockToJSON(blockCapsule.getInstance(), true); + JSONObject hidden = Util.printBlockToJSON(blockCapsule.getInstance(), false); + + // blockID is derived from raw bytes; identical under either flag. + Assert.assertEquals(visible.getString("blockID"), hidden.getString("blockID")); + + // Overall block_header must differ because witness_address is re-encoded. + String headerVisible = visible.getJSONObject("block_header").toJSONString(); + String headerHidden = hidden.getJSONObject("block_header").toJSONString(); + Assert.assertNotEquals(headerVisible, headerHidden); + + // visible=true renders a mainnet address as Base58 starting with 'T'. + String witnessVisible = visible.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessVisible); + Assert.assertTrue("visible=true witness_address should be Base58 ('T...'), got: " + + witnessVisible, witnessVisible.startsWith("T")); + + // visible=false keeps witness_address in raw (non-Base58) form. + String witnessHidden = hidden.getJSONObject("block_header") + .getJSONObject("raw_data").getString("witness_address"); + Assert.assertNotNull(witnessHidden); + Assert.assertNotEquals(witnessVisible, witnessHidden); + } + + @Test + public void testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl() { + // Legacy impl produced JSON via JsonFormat.printToString(block, selfType), + // which omits repeated fields when empty. New impl mirrors that with an + // explicit isEmpty() guard. Pin parity using JsonFormat output as ground + // truth so a future refactor can't quietly start emitting "transactions": [] + // (or stop emitting the key when non-empty). + BlockCapsule empty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + assertTransactionsKeyMatchesLegacy(empty.getInstance(), false); + + BlockCapsule nonEmpty = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + nonEmpty.addTransaction(getTransactionCapsuleExample()); + assertTransactionsKeyMatchesLegacy(nonEmpty.getInstance(), true); + } + + private static void assertTransactionsKeyMatchesLegacy(Protocol.Block block, + boolean expectTransactionsKey) { + JSONObject legacy = JSONObject.parseObject(JsonFormat.printToString(block, true)); + Assert.assertEquals("legacy JsonFormat parity broken — proto behavior changed?", + expectTransactionsKey, legacy.containsKey("transactions")); + + JSONObject actual = Util.printBlockToJSON(block, true); + Assert.assertEquals("new impl diverged from legacy on 'transactions' key presence", + expectTransactionsKey, actual.containsKey("transactions")); + } + + @Test + public void testPrintBlockToJSONCoversAllProtoTopLevelFields() { + // Guards against proto field drift: the old impl delegated to JsonFormat on + // the whole Block message, so any new top-level Block field appeared + // automatically. The new impl hand-assembles the JSON, so a future proto + // field would be silently dropped. Reflect over Block's descriptor and + // assert every declared top-level field is handled. + Map protoFieldToJsonKey = new HashMap<>(); + protoFieldToJsonKey.put("block_header", "block_header"); + // "transactions" is present only when non-empty; parity verified in + // testPrintBlockToJSONTransactionsKeyMatchesLegacyImpl. + protoFieldToJsonKey.put("transactions", "transactions"); + + for (Descriptors.FieldDescriptor f : Protocol.Block.getDescriptor().getFields()) { + Assert.assertTrue( + "Block proto field '" + f.getName() + "' is not handled by printBlockToJSON. " + + "If you added a new top-level field, extend printBlockToJSON and this test.", + protoFieldToJsonKey.containsKey(f.getName())); + } } @Test 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/jsonrpc/BuildArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java index 26699bc63f6..5ad2c85d181 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/BuildArgumentsTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.ObjectMapper; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Before; @@ -29,9 +30,9 @@ public class BuildArgumentsTest extends BaseTest { public void initBuildArgs() { buildArguments = new BuildArguments( "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0",9L,10000L,"",10L, - 2000L,"args",1,"",true); + "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", + "", "", "0", 9L, 10000L, "", 10L, + 2000L, "args", 1, "", true); } @@ -39,15 +40,13 @@ public void initBuildArgs() { public void testBuildArgument() { CallArguments callArguments = new CallArguments( "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0"); - BuildArguments buildArguments = new BuildArguments(callArguments); - Assert.assertEquals(buildArguments.getFrom(), - "0x0000000000000000000000000000000000000000"); - Assert.assertEquals(buildArguments.getTo(), - "0x0000000000000000000000000000000000000001"); - Assert.assertEquals(buildArguments.getGas(), "0x10"); - Assert.assertEquals(buildArguments.getGasPrice(), "0.01"); + "0x0000000000000000000000000000000000000001", "0x10", "0.01", "0x100", + "", "", "0"); + BuildArguments args = new BuildArguments(callArguments); + Assert.assertEquals("0x0000000000000000000000000000000000000000", args.getFrom()); + Assert.assertEquals("0x0000000000000000000000000000000000000001", args.getTo()); + Assert.assertEquals("0x10", args.getGas()); + Assert.assertEquals("0.01", args.getGasPrice()); } @Test @@ -55,19 +54,275 @@ public void testGetContractType() throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { Protocol.Transaction.Contract.ContractType contractType = buildArguments.getContractType(wallet); - Assert.assertEquals(contractType, Protocol.Transaction.Contract.ContractType.TransferContract); + Assert.assertEquals(Protocol.Transaction.Contract.ContractType.TransferContract, contractType); } @Test public void testParseValue() throws JsonRpcInvalidParamsException { long value = buildArguments.parseValue(); - Assert.assertEquals(value, 256L); + Assert.assertEquals(256L, value); } @Test public void testParseGas() throws JsonRpcInvalidParamsException { long gas = buildArguments.parseGas(); - Assert.assertEquals(gas, 16L); + Assert.assertEquals(16L, gas); + } + + @Test + public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOnly_returnsData() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0xcafebabe"); + Assert.assertEquals("0xcafebabe", args.resolveData()); + } + + @Test + public void resolveData_bothPresentSame_returnsValue() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0xfeedface"); + args.setInput("0xfeedface"); + Assert.assertEquals("0xfeedface", args.resolveData()); + } + + /** Pins that "0x" on both sides decodes to []==[] and is not a conflict. */ + @Test + public void resolveData_bothZeroX_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0x"); + args.setData("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_inputZeroXOnly_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_dataZeroXOnly_returnsZeroX() throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0x"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_caseDifference_returnsInput() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setInput("0xDEADbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0xDEADbeef", args.resolveData()); + } + + /** + * Pins geth-equivalent semantics: empty string is presence with + * empty bytes, so paired with non-empty data the byte values differ + * and the build path raises the geth setDefaults conflict at the + * {@code getContractType()} entry point. + */ + @Test + public void getContractType_inputEmptyDataNonEmpty_throwsConflict() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput(""); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + } + + /** + * Wording matches go-ethereum's setDefaults so existing tooling can + * detect the error string. + */ + @Test + public void getContractType_inputAndDataConflict_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + + JsonRpcInvalidParamsException ex = Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + Assert.assertTrue( + "error message should match go-ethereum's wording: " + ex.getMessage(), + ex.getMessage().contains("both \"data\" and \"input\" are set and not equal")); + } + + @Test + public void getContractType_inputZeroXDataNonEmpty_throwsConflict() { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0x"); + args.setData("0xdeadbeef"); + + Assert.assertThrows( + JsonRpcInvalidParamsException.class, + () -> args.getContractType(wallet)); + } + + @Test + public void getContractType_inputAndDataEqual_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + /** Reproduces issue #6517 contract-creation symptom on the build path. */ + @Test + public void getContractType_createSmartContractViaInput_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + @Test + public void copyConstructor_preservesBothInputAndData() { + CallArguments src = new CallArguments(); + src.setFrom("0x0000000000000000000000000000000000000001"); + src.setData("0xcafebabe"); + src.setInput("0xdeadbeef"); + + BuildArguments copy = new BuildArguments(src); + Assert.assertEquals("0xcafebabe", copy.getData()); + Assert.assertEquals("0xdeadbeef", copy.getInput()); + } + + @Test + public void copyConstructor_propagatesConflictToBuildPath() { + CallArguments src = new CallArguments(); + src.setData("0xcafebabe"); + src.setInput("0xdeadbeef"); + + BuildArguments copy = new BuildArguments(src); + Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> copy.getContractType(wallet)); + } + + @Test + public void deserializeWithInputField_succeedsAndResolvesToInput() throws Exception { + String json = "{\"from\":\"0x0000000000000000000000000000000000000001\"," + + "\"to\":\"0x0000000000000000000000000000000000000002\"," + + "\"input\":\"0xdeadbeef\"}"; + BuildArguments args = new ObjectMapper().readValue(json, BuildArguments.class); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + Assert.assertEquals("0xdeadbeef", args.getInput()); + Assert.assertNull(args.getData()); + } + + /** + * Regression guard: a future {@code getXxx} rename would expose + * {@code resolveData} as a wire property and risk throwing during + * serialisation. + */ + @Test + public void jacksonSerialize_doesNotExposeResolveDataOrThrowOnConflict() + throws Exception { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); // conflicting bytes, would throw if resolveData() were invoked + String json = new ObjectMapper().writeValueAsString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Same guarantee for FastJSON, which also discovers bean getters. */ + @Test + public void fastjsonSerialize_doesNotExposeResolveDataOrThrowOnConflict() { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + String json = com.alibaba.fastjson.JSON.toJSONString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Validates the loser field too, not only the precedence winner. */ + @Test + public void resolveData_inputValidDataMalformed_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataValid_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xzz"); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataAbsent_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_dataMalformedInputAbsent_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code input} is the new spec-aligned field: missing {@code 0x} prefix + * is rejected per the execution-apis BYTES schema. + */ + @Test + public void resolveData_inputNoPrefix_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("deadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputOddLength_throwsInvalidParams() { + BuildArguments args = new BuildArguments(); + args.setInput("0x123"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code data} is the legacy field: bare hex (no {@code 0x} prefix) + * stays accepted for backward compatibility with existing callers + * (e.g. BuildTransactionTest.testCreateSmartContract). + */ + @Test + public void resolveData_dataNoPrefix_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("deadbeef"); + Assert.assertEquals("deadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOddLength_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + BuildArguments args = new BuildArguments(); + args.setData("0x123"); + Assert.assertEquals("0x123", args.resolveData()); } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java index 2148e1a2fe0..66fb8e0a0c7 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/CallArgumentsTest.java @@ -1,5 +1,6 @@ package org.tron.core.services.jsonrpc; +import com.fasterxml.jackson.databind.ObjectMapper; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Before; @@ -26,9 +27,11 @@ public class CallArgumentsTest extends BaseTest { @Before public void init() { - callArguments = new CallArguments("0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000001","0x10","0.01","0x100", - "","0"); + callArguments = new CallArguments( + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000001", + "0x10", "0.01", "0x100", + "", "", "0"); } @Test @@ -44,4 +47,200 @@ public void testParseValue() throws JsonRpcInvalidParamsException { Assert.assertEquals(256L, value); } + @Test + public void resolveData_inputOnly_returnsInput() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOnly_returnsData() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xcafebabe"); + Assert.assertEquals("0xcafebabe", args.resolveData()); + } + + @Test + public void resolveData_bothPresentSame_returnsValue() throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xfeedface"); + args.setInput("0xfeedface"); + Assert.assertEquals("0xfeedface", args.resolveData()); + } + + @Test + public void resolveData_bothPresentDifferent_inputWinsNoError() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0xcafebabe"); + args.setInput("0xdeadbeef"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_inputIsZeroX_dataNonEmpty_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0x"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0x", args.resolveData()); + } + + @Test + public void resolveData_dataIsZeroX_inputNonEmpty_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0x"); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + } + + @Test + public void resolveData_caseDifference_returnsInput() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput("0xDEADbeef"); + args.setData("0xdeadbeef"); + Assert.assertEquals("0xDEADbeef", args.resolveData()); + } + + /** Pins geth-equivalent semantics: "" is presence, wins over data by precedence. */ + @Test + public void resolveData_inputEmpty_dataNonEmpty_inputWinsAsEmpty() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setInput(""); + args.setData("0xdeadbeef"); + Assert.assertEquals("", args.resolveData()); + } + + @Test + public void resolveData_neitherPresent_returnsNull() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + Assert.assertNull(args.resolveData()); + } + + /** Validates the loser field too, not only the precedence winner. */ + @Test + public void resolveData_inputValidDataMalformed_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataValid_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xzz"); + args.setData("0xdeadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputMalformedDataAbsent_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_dataMalformedInputAbsent_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setData("0xzz"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code input} is the new spec-aligned field: missing {@code 0x} prefix + * is rejected per the execution-apis BYTES schema. + */ + @Test + public void resolveData_inputNoPrefix_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("deadbeef"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + @Test + public void resolveData_inputOddLength_throwsInvalidParams() { + CallArguments args = new CallArguments(); + args.setInput("0x123"); + Assert.assertThrows(JsonRpcInvalidParamsException.class, args::resolveData); + } + + /** + * {@code data} is the legacy field: bare hex (no {@code 0x} prefix) + * stays accepted for backward compatibility with existing callers + * (e.g. BuildTransactionTest.testCreateSmartContract). + */ + @Test + public void resolveData_dataNoPrefix_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("deadbeef"); + Assert.assertEquals("deadbeef", args.resolveData()); + } + + @Test + public void resolveData_dataOddLength_acceptedForBackwardCompat() + throws JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setData("0x123"); + Assert.assertEquals("0x123", args.resolveData()); + } + + /** Reproduces issue #6517 contract-creation symptom. */ + @Test + public void getContractType_createSmartContractViaInput_succeeds() + throws JsonRpcInvalidRequestException, JsonRpcInvalidParamsException { + CallArguments args = new CallArguments(); + args.setFrom("0x0000000000000000000000000000000000000001"); + args.setInput("0xdeadbeef"); + Assert.assertEquals( + Protocol.Transaction.Contract.ContractType.CreateSmartContract, + args.getContractType(wallet)); + } + + /** Reproduces issue #6517 Jackson parse-error symptom. */ + @Test + public void deserializeWithInputField_succeedsAndResolvesToInput() throws Exception { + String json = "{\"from\":\"0x0000000000000000000000000000000000000001\"," + + "\"to\":\"0x0000000000000000000000000000000000000002\"," + + "\"input\":\"0xdeadbeef\"}"; + CallArguments args = new ObjectMapper().readValue(json, CallArguments.class); + Assert.assertEquals("0xdeadbeef", args.resolveData()); + Assert.assertEquals("0xdeadbeef", args.getInput()); + Assert.assertNull(args.getData()); + } + + /** + * Regression guard: a future {@code getXxx} rename would expose + * {@code resolveData} as a wire property and risk throwing during + * serialisation. + */ + @Test + public void jacksonSerialize_doesNotExposeResolveDataOrThrowOnConflict() + throws Exception { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); // would throw conflict in build path + String json = new ObjectMapper().writeValueAsString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + + /** Same guarantee for FastJSON, which also discovers bean getters. */ + @Test + public void fastjsonSerialize_doesNotExposeResolveDataOrThrowOnConflict() { + CallArguments args = new CallArguments(); + args.setInput("0xdeadbeef"); + args.setData("0xcafebabe"); + String json = com.alibaba.fastjson.JSON.toJSONString(args); + Assert.assertFalse("should not leak resolveData: " + json, + json.contains("resolveData")); + } + } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java new file mode 100644 index 00000000000..6aaeea2cc4e --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/JsonRpcApiUtilTest.java @@ -0,0 +1,78 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public class JsonRpcApiUtilTest { + + @Test + public void parseBlockNumberAcceptsHex() throws JsonRpcInvalidParamsException { + assertEquals(0x1aL, JsonRpcApiUtil.parseBlockNumber("0x1a")); + assertEquals(0L, JsonRpcApiUtil.parseBlockNumber("0x0")); + } + + @Test + public void parseBlockNumberAcceptsDecimal() throws JsonRpcInvalidParamsException { + assertEquals(12345L, JsonRpcApiUtil.parseBlockNumber("12345")); + } + + @Test + public void parseBlockNumberAcceptsMaxLongValue() throws JsonRpcInvalidParamsException { + assertEquals(Long.MAX_VALUE, + JsonRpcApiUtil.parseBlockNumber("0x7fffffffffffffff")); + } + + @Test + public void parseBlockNumberRejectsNegative() { + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("-1")); + assertEquals("invalid block number", e1.getMessage()); + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x-1")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOverflow() { + // 2^64 - 1: fits uint64 but overflows signed long + JsonRpcInvalidParamsException e1 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xffffffffffffffff")); + assertEquals("invalid block number", e1.getMessage()); + // 2^63: just past Long.MAX_VALUE + JsonRpcInvalidParamsException e2 = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0x8000000000000000")); + assertEquals("invalid block number", e2.getMessage()); + } + + @Test + public void parseBlockNumberRejectsOversized() { + // 101 chars exceeds the 100-char limit + String tooLong = "0x" + new String(new char[99]).replace('\0', 'a'); + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(tooLong)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsNull() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber(null)); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsMalformedHex() { + JsonRpcInvalidParamsException e = assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("0xGG")); + assertEquals("invalid block number", e.getMessage()); + } + + @Test + public void parseBlockNumberRejectsEmpty() { + assertThrows(JsonRpcInvalidParamsException.class, + () -> JsonRpcApiUtil.parseBlockNumber("")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java new file mode 100644 index 00000000000..4150275c5e2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/LogFilterWrapperStrategyTest.java @@ -0,0 +1,162 @@ +package org.tron.core.services.jsonrpc; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.Wallet; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.jsonrpc.TronJsonRpc.FilterRequest; +import org.tron.core.services.jsonrpc.filters.LogFilterWrapper; + +/** + * Verify LogFilterWrapper strategies match develop branch behavior. + * + * Four filter strategies based on parameter emptiness (develop branch semantics): + * - Strategy 1: Both fromBlock and toBlock are empty -> (currentMaxBlockNum, Long.MAX_VALUE) + * - Strategy 2: fromBlock empty, toBlock non-empty -> based on toBlock value + * - Strategy 3: fromBlock non-empty, toBlock empty -> (fromBlock, Long.MAX_VALUE) + * - Strategy 4: Both non-empty -> parse both, handle "latest" using snapshot + */ +public class LogFilterWrapperStrategyTest { + + private Wallet mockWallet; + private static final long CURRENT_MAX_BLOCK = 81628775L; + + @Before + public void setUp() { + mockWallet = mock(Wallet.class); + when(mockWallet.getHeadBlockNum()).thenReturn(CURRENT_MAX_BLOCK); + when(mockWallet.getSolidBlockNum()).thenReturn(CURRENT_MAX_BLOCK - 100); + } + + private LogFilterWrapper createFilter(String fromBlock, String toBlock) throws Exception { + FilterRequest request = new FilterRequest(fromBlock, toBlock, null, null, null); + return new LogFilterWrapper(request, CURRENT_MAX_BLOCK, mockWallet, false); + } + + @Test + public void testStrategy1_BothNull() throws Exception { + LogFilterWrapper filter = createFilter(null, null); + assertEquals("fromBlock should be currentMaxBlockNum", CURRENT_MAX_BLOCK, + filter.getFromBlock()); + assertEquals("toBlock should be Long.MAX_VALUE", Long.MAX_VALUE, + filter.getToBlock()); + } + + @Test + public void testStrategy1_BothEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("", ""); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyToHex() throws Exception { + // toBlock = 0x100 = 256 + // fromBlock = min(256, CURRENT_MAX_BLOCK) = 256 + LogFilterWrapper filter = createFilter(null, "0x100"); + assertEquals(256L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyToLatest() throws Exception { + // toBlock = "latest" is treated as Long.MAX_VALUE in Strategy 2 + // fromBlock = min(Long.MAX_VALUE, CURRENT_MAX_BLOCK) = CURRENT_MAX_BLOCK + LogFilterWrapper filter = createFilter(null, "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy2_FromEmptyStringToHex() throws Exception { + LogFilterWrapper filter = createFilter("", "0x200"); + assertEquals(512L, filter.getFromBlock()); + assertEquals(512L, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromHexToEmpty() throws Exception { + // fromBlock = 0x1 = 1 + // toBlock = Long.MAX_VALUE (tracking future blocks) + LogFilterWrapper filter = createFilter("0x1", null); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromLatestToEmpty() throws Exception { + // fromBlock = "latest" (using snapshot) = currentMaxBlockNum + // toBlock = Long.MAX_VALUE + LogFilterWrapper filter = createFilter("latest", null); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy3_FromHexToEmptyString() throws Exception { + LogFilterWrapper filter = createFilter("0x5", ""); + assertEquals(5L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_BothHex() throws Exception { + // fromBlock = 1, toBlock = 256 + LogFilterWrapper filter = createFilter("0x1", "0x100"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(256L, filter.getToBlock()); + } + + @Test + public void testStrategy4_BothLatest() throws Exception { + // Both "latest" are non-empty, so Strategy 4. + // fromBlock "latest" -> currentMaxBlockNum (snapshot). toBlock "latest" -> Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("latest", "latest"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromHexToLatest() throws Exception { + // fromBlock = 0x1 (concrete). toBlock = "latest" resolves to Long.MAX_VALUE. + LogFilterWrapper filter = createFilter("0x1", "latest"); + assertEquals(1L, filter.getFromBlock()); + assertEquals(Long.MAX_VALUE, filter.getToBlock()); + } + + @Test + public void testStrategy4_FromLatestToHexAboveLatest() throws Exception { + // This test requires a toBlock value larger than currentMaxBlockNum + // Using 0x5000000 (83886080) which is > 81628775 + LogFilterWrapper filter = createFilter("latest", "0x5000000"); + assertEquals(CURRENT_MAX_BLOCK, filter.getFromBlock()); + assertEquals(83886080L, filter.getToBlock()); + } + + @Test + public void testStrategy4_InvertedRangeThrows() throws Exception { + // fromBlock (0x100 = 256) > toBlock (0x1 = 1) should throw + try { + createFilter("0x100", "0x1"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } + + @Test + public void testStrategy4_LatestGreaterThanSmallBlock_Throws() throws Exception { + // fromBlock = "latest" (currentMaxBlockNum = 81628775) > toBlock (0x100 = 256) should throw + try { + createFilter("latest", "0x100"); + Assert.fail("Expected exception"); + } catch (JsonRpcInvalidParamsException e) { + assertEquals("please verify: fromBlock <= toBlock", e.getMessage()); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java index a53a32daf45..e9cb8b7e274 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionReceiptTest.java @@ -32,6 +32,7 @@ public void testTransactionReceipt() throws JsonRpcInternalException { Protocol.TransactionInfo transactionInfo = Protocol.TransactionInfo.newBuilder() .setId(ByteString.copyFrom("1".getBytes())) .setContractAddress(ByteString.copyFrom("address1".getBytes())) + .setBlockTimeStamp(1000000L) .setReceipt(Protocol.ResourceReceipt.newBuilder() .setEnergyUsageTotal(100L) .setResult(Protocol.Transaction.Result.contractResult.DEFAULT) @@ -53,8 +54,11 @@ public void testTransactionReceipt() throws JsonRpcInternalException { Protocol.Block block = Protocol.Block.newBuilder().setBlockHeader( Protocol.BlockHeader.newBuilder().setRawData( - Protocol.BlockHeader.raw.newBuilder().setNumber(1))).addTransactions( - transaction).build(); + Protocol.BlockHeader.raw.newBuilder() + .setNumber(1) + .setTimestamp(1000000L))) + .addTransactions(transaction) + .build(); BlockCapsule blockCapsule = new BlockCapsule(block); long energyFee = wallet.getEnergyFee(blockCapsule.getTimeStamp()); @@ -65,35 +69,35 @@ public void testTransactionReceipt() throws JsonRpcInternalException { new TransactionReceipt(blockCapsule, transactionInfo, context, energyFee); Assert.assertNotNull(transactionReceipt); - String blockHash = "0x0000000000000001464f071c8a336fd22eb5145dff1b245bda013ec89add8497"; + String blockHash = "0x0000000000000001ba51f50f562758a449ff4a98df4febef89e122c1bb7e1a0c"; // assert basic fields - Assert.assertEquals(transactionReceipt.getBlockHash(), blockHash); - Assert.assertEquals(transactionReceipt.getBlockNumber(), "0x1"); - Assert.assertEquals(transactionReceipt.getTransactionHash(), "0x31"); - Assert.assertEquals(transactionReceipt.getTransactionIndex(), "0x0"); - Assert.assertEquals(transactionReceipt.getCumulativeGasUsed(), ByteArray.toJsonHex(102)); - Assert.assertEquals(transactionReceipt.getGasUsed(), ByteArray.toJsonHex(100)); - Assert.assertEquals(transactionReceipt.getEffectiveGasPrice(), ByteArray.toJsonHex(energyFee)); - Assert.assertEquals(transactionReceipt.getStatus(), "0x1"); + Assert.assertEquals(blockHash, transactionReceipt.getBlockHash()); + Assert.assertEquals("0x1", transactionReceipt.getBlockNumber()); + Assert.assertEquals("0x31", transactionReceipt.getTransactionHash()); + Assert.assertEquals("0x0", transactionReceipt.getTransactionIndex()); + Assert.assertEquals(ByteArray.toJsonHex(102), transactionReceipt.getCumulativeGasUsed()); + Assert.assertEquals(ByteArray.toJsonHex(100), transactionReceipt.getGasUsed()); + Assert.assertEquals(ByteArray.toJsonHex(energyFee), transactionReceipt.getEffectiveGasPrice()); + Assert.assertEquals("0x1", transactionReceipt.getStatus()); // assert contract fields - Assert.assertEquals(transactionReceipt.getFrom(), ByteArray.toJsonHexAddress(new byte[0])); - Assert.assertEquals(transactionReceipt.getTo(), ByteArray.toJsonHexAddress(new byte[0])); + Assert.assertEquals(ByteArray.toJsonHexAddress(new byte[0]), transactionReceipt.getFrom()); + Assert.assertEquals(ByteArray.toJsonHexAddress(new byte[0]), transactionReceipt.getTo()); Assert.assertNull(transactionReceipt.getContractAddress()); // assert logs fields - Assert.assertEquals(transactionReceipt.getLogs().length, 1); - Assert.assertEquals(transactionReceipt.getLogs()[0].getLogIndex(), "0x3"); - Assert.assertEquals( - transactionReceipt.getLogs()[0].getBlockHash(), blockHash); - Assert.assertEquals(transactionReceipt.getLogs()[0].getBlockNumber(), "0x1"); - Assert.assertEquals(transactionReceipt.getLogs()[0].getTransactionHash(), "0x31"); - Assert.assertEquals(transactionReceipt.getLogs()[0].getTransactionIndex(), "0x0"); + Assert.assertEquals(1, transactionReceipt.getLogs().length); + Assert.assertEquals("0x3", transactionReceipt.getLogs()[0].getLogIndex()); + Assert.assertEquals(blockHash, transactionReceipt.getLogs()[0].getBlockHash()); + Assert.assertEquals("0x1", transactionReceipt.getLogs()[0].getBlockNumber()); + Assert.assertEquals("0x31", transactionReceipt.getLogs()[0].getTransactionHash()); + Assert.assertEquals("0x0", transactionReceipt.getLogs()[0].getTransactionIndex()); + Assert.assertEquals("0x3e8", transactionReceipt.getLogs()[0].getBlockTimestamp()); // assert default fields Assert.assertNull(transactionReceipt.getRoot()); - Assert.assertEquals(transactionReceipt.getType(), "0x0"); - Assert.assertEquals(transactionReceipt.getLogsBloom(), ByteArray.toJsonHex(new byte[256])); + Assert.assertEquals("0x0", transactionReceipt.getType()); + Assert.assertEquals(ByteArray.toJsonHex(new byte[256]), transactionReceipt.getLogsBloom()); } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java index 4e1af06199c..19c2bb6c4d3 100644 --- a/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TransactionResultTest.java @@ -23,10 +23,18 @@ public class TransactionResultTest extends BaseTest { private static final String OWNER_ADDRESS = "41548794500882809695a8a687866e76d4271a1abc"; private static final String CONTRACT_ADDRESS = "A0B4750E2CD76E19DCA331BF5D089B71C3C2798548"; + // QUANTITY pattern from ethereum/execution-apis base-types schema (uint). + private static final String QUANTITY_PATTERN = "^0x(0|[1-9a-f][0-9a-f]*)$"; + static { Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); } + private static void assertQuantity(String value) { + Assert.assertNotNull(value); + Assert.assertTrue("not a valid QUANTITY: " + value, value.matches(QUANTITY_PATTERN)); + } + @Test public void testBuildTransactionResultWithBlock() { SmartContractOuterClass.TriggerSmartContract.Builder builder2 = @@ -49,6 +57,8 @@ public void testBuildTransactionResultWithBlock() { transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x1"); Assert.assertEquals(transactionResult.getGas(), "0x64"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } @Test @@ -65,7 +75,8 @@ public void testBuildTransactionResult() { Assert.assertEquals("0x5691531881bc44adbc722060d85fdf29265823db8e884b0d104fcfbba253cf11", transactionResult.getHash()); Assert.assertEquals(transactionResult.getGasPrice(), "0x"); - Assert.assertEquals(transactionResult.getNonce(), "0x0000000000000000"); + Assert.assertEquals("0x0", transactionResult.getNonce()); + assertQuantity(transactionResult.getNonce()); } } diff --git a/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java new file mode 100644 index 00000000000..8d72aeed04f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/jsonrpc/TronJsonRpcRevertReasonTest.java @@ -0,0 +1,97 @@ +package org.tron.core.services.jsonrpc; + +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.utils.ByteArray; + +public class TronJsonRpcRevertReasonTest { + + @Test + public void testTryDecodeRevertReasonWithMalformedLength() { + // Error(string) selector + offset=0x20 + length=0x7FFFFFFF + 3 bytes of payload. + // parseDataBytes throws because the declared length exceeds the buffer. + // The helper should return "" and leave the raw revert hex untouched. + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "000000000000000000000000000000000000000000000000000000007fffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNegativeLength() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "414243"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithValidData() { + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000016" + + "6e6f7420656e6f75676820696e7075742076616c756500000000000000000000"); + Assert.assertEquals(": not enough input value", + TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithEmptyString() { + // require(cond, "") yields a empty string + byte[] resData = ByteArray.fromHexString("08c379a0" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000000"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithOversizedPayload() { + // selector(4) + payload(4097) one byte over the 4096 limit: must be rejected before parse. + byte[] resData = new byte[4101]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonWithNullData() { + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(null)); + } + + @Test + public void testTryDecodeRevertReasonWithShortSelector() { + // length == selector length (4): not enough bytes for any payload, reject. + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(new byte[]{ + 0x08, (byte) 0xc3, 0x79, (byte) 0xa0})); + } + + @Test + public void testTryDecodeRevertReasonWithNonErrorSelector() { + // Non-Error(string) selector (e.g. Panic(uint256) = 0x4e487b71) must be rejected. + byte[] resData = ByteArray.fromHexString("4e487b71" + + "0000000000000000000000000000000000000000000000000000000000000001"); + Assert.assertEquals("", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } + + @Test + public void testTryDecodeRevertReasonAtPayloadLimit() { + // selector(4) + payload(4096) exactly at the limit: must go through parse, not size-reject. + byte[] resData = new byte[4100]; + resData[0] = 0x08; + resData[1] = (byte) 0xc3; + resData[2] = 0x79; + resData[3] = (byte) 0xa0; + // ABI offset = 0x20 + resData[4 + 31] = 0x20; + // ABI string length = 2 + resData[4 + 32 + 31] = 0x02; + // data "ok", remaining bytes stay zero-padded + resData[4 + 64] = 'o'; + resData[4 + 65] = 'k'; + Assert.assertEquals(": ok", TronJsonRpcImpl.tryDecodeRevertReason(resData)); + } +} 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/vm/repository/RepositoryImplHardenTest.java b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java new file mode 100644 index 00000000000..6b15409edd6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java @@ -0,0 +1,280 @@ +package org.tron.core.vm.repository; + +import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; +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.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.VMConfig; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class RepositoryImplHardenTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + private RepositoryImpl repository; + private Method increaseMethod; + private Method getUsageMethod; + private Method usageToBalanceMethod; + + @Before + public void setUp() throws Exception { + repository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + + increaseMethod = RepositoryImpl.class.getDeclaredMethod( + "increase", long.class, long.class, long.class, long.class, long.class); + increaseMethod.setAccessible(true); + + getUsageMethod = RepositoryImpl.class.getDeclaredMethod( + "getUsage", long.class, long.class); + getUsageMethod.setAccessible(true); + + usageToBalanceMethod = RepositoryImpl.class.getDeclaredMethod( + "usageToBalance", long.class, long.class, long.class); + usageToBalanceMethod.setAccessible(true); + } + + @After + public void tearDown() { + VMConfig.initAllowHardenResourceCalculation(0); + } + + private long invokeIncrease(long lastUsage, long usage, long lastTime, + long now, long windowSize) throws Exception { + try { + return (long) increaseMethod.invoke( + repository, lastUsage, usage, lastTime, now, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeGetUsage(long usage, long windowSize) throws Exception { + try { + return (long) getUsageMethod.invoke(repository, usage, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeUsageToBalance(long usage, long totalWeight, long totalLimit) + throws Exception { + try { + return (long) usageToBalanceMethod.invoke( + repository, usage, totalWeight, totalLimit); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + @Test + public void testIncreaseNormalValuesParity() throws Exception { + long lastUsage = 1_000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGetUsageNormalValuesParity() throws Exception { + long usage = 100_000L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeGetUsage(usage, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeGetUsage(usage, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeIncrease(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() throws Exception { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testGetUsageCorrectAcrossOverflowBoundary() throws Exception { + long usage = Long.MAX_VALUE / 1000; // ~9.2e15 + long windowSize = 28800L; + + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + + VMConfig.initAllowHardenResourceCalculation(0); + long wrapped = invokeGetUsage(usage, windowSize); + Assert.assertNotEquals(expected, wrapped); + } + + @Test + public void testGetUsageLargeButSafeWithHardening() throws Exception { + long usage = 500_000_000_000L; // 5e11 + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + } + + + @Test + public void testUsageToBalanceParity() throws Exception { + long usage = 1_000_000L; + long totalWeight = 2_000_000_000L; + long totalLimit = 50_000_000_000L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeUsageToBalance(usage, totalWeight, totalLimit); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUsageToBalanceCorrectAcrossDoublePrecision() throws Exception { + long usage = 100_000_000L; // 1e8 + long totalWeight = 100_000_000_000L; // 1e11 -> usage * weight = 1e19, beyond 2^53 + long totalLimit = 50_000_000_000L; + + java.math.BigInteger expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(totalWeight)) + .multiply(java.math.BigInteger.valueOf(1_000_000L)) + .divide(java.math.BigInteger.valueOf(totalLimit)); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testUsageToBalanceOverflowDetectedWithHardening() { + long usage = 1_000_000_000L; + long totalWeight = 1_000_000_000_000L; + long totalLimit = 1L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeUsageToBalance(usage, totalWeight, totalLimit)); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedParityWithNonIntegerRatio() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = repository.calculateGlobalEnergyLimit(account); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = repository.calculateGlobalEnergyLimit(account); + + long expected = java.math.BigInteger.valueOf(10000L) + .multiply(java.math.BigInteger.valueOf(totalEnergyLimit)) + .divide(java.math.BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + Assert.assertEquals(expected, resultNew); + Assert.assertEquals(resultOld, resultNew); + + long buggy = 10000L * (totalEnergyLimit / totalEnergyWeight); + Assert.assertNotEquals(buggy, resultNew); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedOverflowDetected() { + long totalEnergyLimit = Long.MAX_VALUE / 2; + long totalEnergyWeight = 1L; + long frozeBalance = Long.MAX_VALUE / 4; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(1); + Assert.assertThrows(ArithmeticException.class, + () -> repository.calculateGlobalEnergyLimit(account)); + } +} 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/core/zksnark/MerkleContainerTest.java b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java index ed52e014a7b..61fb36a9f68 100644 --- a/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java @@ -3,7 +3,9 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import javax.annotation.Resource; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; @@ -38,10 +40,23 @@ public class MerkleContainerTest extends BaseTest { // private static MerkleContainer merkleContainer; + private static boolean origShieldedApi; + static { Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + /*@Before public void init() { merkleContainer = MerkleContainer 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/framework/src/test/resources/logback-test.xml b/framework/src/test/resources/logback-test.xml index cc8c84e831f..54462a6761d 100644 --- a/framework/src/test/resources/logback-test.xml +++ b/framework/src/test/resources/logback-test.xml @@ -1,5 +1,9 @@ + + true + + @@ -31,12 +35,13 @@ - + + diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 86880157f35..983528f0002 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -289,12 +289,12 @@ - - - + + + - - + + @@ -315,12 +315,12 @@ - - - + + + - - + + @@ -347,12 +347,12 @@ - - - + + + - - + + @@ -363,9 +363,9 @@ - - - + + + @@ -386,12 +386,12 @@ - - - + + + - - + + @@ -404,9 +404,9 @@ - - - + + + @@ -417,12 +417,12 @@ - - - + + + - - + + @@ -467,15 +467,15 @@ - - - + + + - - + + - - + + @@ -508,14 +508,14 @@ - - - + + + - - - + + + @@ -539,12 +539,12 @@ - - - + + + - - + + @@ -888,194 +888,191 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - + + + - - + + + + + + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -1654,12 +1651,12 @@ - - - + + + - - + + @@ -1699,12 +1696,12 @@ - - - + + + - - + + @@ -1712,9 +1709,9 @@ - - - + + + @@ -1722,9 +1719,9 @@ - - - + + + @@ -1751,65 +1748,65 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -2012,6 +2009,17 @@ + + + + + + + + + + + @@ -2044,6 +2052,22 @@ + + + + + + + + + + + + + + + + @@ -2168,17 +2192,17 @@ - - - + + + - - + + - - - + + + @@ -2221,12 +2245,20 @@ - - - + + + + + + + + + + + - - + + @@ -2255,21 +2287,6 @@ - - - - - - - - - - - - - - - diff --git a/plugins/README.md b/plugins/README.md index db25811882f..f14e070c01a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -75,16 +75,20 @@ DB lite provides lite database, parameters are compatible with previous `LiteFul - `-fn | --fn-data-path`: The database path to be split or merged. - `-ds | --dataset-path`: When operation is `split`,`dataset-path` is the path that store the `snapshot` or `history`, when operation is `split`, `dataset-path` is the `history` data path. +- `--exclude-historical-balance`: Only used with `operate=split -t snapshot`, default: false. When set to true, `balance-trace` and `account-trace` are excluded from the lite snapshot. The flag has functional impact only when the source full node ran with `historyBalanceLookup=true` (off by default; most operators are unaffected). **WARNING:** for nodes that had `historyBalanceLookup=true`, this loss is permanent — a lite node booted from such a snapshot cannot safely serve historical balance lookups (`getBlockBalance` may fail, and `getAccountBalance` may return `balance=0` when `account-trace` data is missing), and running `merge` afterwards will NOT restore the feature. If you need historical balance lookup on the resulting lite node, do **not** enable this flag. `split -t history` and `merge` ignore this flag. - `-h | --help`: Provide the help info. ### Examples: ```shell script # full command - java -jar Toolkit.jar db lite [-h] -ds= -fn= [-o=] [-t=] + java -jar Toolkit.jar db lite [-h] -ds= -fn= [-o=] [-t=] [--exclude-historical-balance] # examples #split and get a snapshot dataset java -jar Toolkit.jar db lite -o split -t snapshot --fn-data-path output-directory/database --dataset-path /tmp + #split and get a snapshot dataset without balance-trace / account-trace (smaller snapshot; + #historical balance lookup cannot be safely served on the resulting lite node) + java -jar Toolkit.jar db lite -o split -t snapshot --fn-data-path output-directory/database --dataset-path /tmp --exclude-historical-balance #split and get a history dataset java -jar Toolkit.jar db lite -o split -t history --fn-data-path output-directory/database --dataset-path /tmp #merge history dataset and snapshot dataset @@ -143,3 +147,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..09a13a19b1b 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -34,18 +34,37 @@ 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' + // x86 declares io.github.tronprotocol:leveldbjni-all:1.18.2 below; + // :crypto -> :common -> :platform also transitively pulls + // org.fusesource.leveldbjni:leveldbjni-all:1.8. Both jars carry + // org/iq80/leveldb/Options.class and the fat jar's last-write-wins + // merge can leave the 1.8 copy, which lacks Options.maxBatchSize(int) + // and breaks `db archive` at runtime. Drop the 1.8 transitive on x86 + // only; ARM64 has a single copy via :platform direct and no conflict. + if (!rootProject.archInfo.isArm64) { + exclude group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all' + } + } 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' - implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.79' + implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.84' if (rootProject.archInfo.isArm64) { testRuntimeOnly group: 'org.fusesource.hawtjni', name: 'hawtjni-runtime', version: '1.18' // for test implementation project(":platform") } else { implementation project(":platform"), { + // Only leveldbjni-all is excluded; the io.github.tronprotocol + // 1.18.2 fork below is the version we want on x86. zksnark-java-sdk + // and commons-io are intentionally kept (the plugins test runtime + // needs both via :crypto -> :common -> :platform and the duplicate + // resolution dedups to one copy). exclude(group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all') - exclude(group: 'io.github.tronprotocol', module: 'zksnark-java-sdk') - exclude(group: 'commons-io', module: 'commons-io') } implementation 'io.github.tronprotocol:leveldbjni-all:1.18.2' implementation 'io.github.tronprotocol:leveldb:1.18.2' @@ -124,7 +143,13 @@ def binaryRelease(taskName, jarName, mainClass) { from(sourceSets.main.output) { include "/**" } - dependsOn (project(':protocol').jar, project(':platform').jar) // explicit_dependency + // Fat jar zips up runtimeClasspath, which includes the jar outputs of + // every project dependency. Declare them all explicitly so Gradle does + // not warn about implicit_dependency and disable execution optimizations + // (and so partial / parallel builds cannot run binaryRelease before the + // dependency jars exist). + dependsOn (project(':protocol').jar, project(':platform').jar, + project(':crypto').jar, project(':common').jar) // explicit_dependency from { configurations.runtimeClasspath.collect { // https://docs.gradle.org/current/userguide/upgrading_version_6.html#changes_6.3 it.isDirectory() ? it : zipTree(it) diff --git a/plugins/src/main/java/common/org/tron/plugins/DbLite.java b/plugins/src/main/java/common/org/tron/plugins/DbLite.java index 3f8a6cb58c8..1a7e4e270f7 100644 --- a/plugins/src/main/java/common/org/tron/plugins/DbLite.java +++ b/plugins/src/main/java/common/org/tron/plugins/DbLite.java @@ -20,6 +20,7 @@ import java.util.concurrent.Callable; import java.util.stream.Collectors; import java.util.stream.LongStream; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import me.tongfei.progressbar.ProgressBar; import org.rocksdb.RocksDBException; @@ -57,6 +58,8 @@ public class DbLite implements Callable { private static final String TRANSACTION_HISTORY_DB_NAME = "transactionHistoryStore"; private static final String PROPERTIES_DB_NAME = "properties"; private static final String TRANS_CACHE_DB_NAME = "trans-cache"; + private static final String BALANCE_TRACE_DB_NAME = "balance-trace"; + private static final String ACCOUNT_TRACE_DB_NAME = "account-trace"; private static final List archiveDbs = Arrays.asList( BLOCK_DB_NAME, @@ -65,6 +68,10 @@ public class DbLite implements Callable { TRANSACTION_RET_DB_NAME, TRANSACTION_HISTORY_DB_NAME); + private static final List traceDbs = Arrays.asList( + BALANCE_TRACE_DB_NAME, + ACCOUNT_TRACE_DB_NAME); + enum Operate { split, merge } enum Type { snapshot, history } @@ -105,8 +112,26 @@ enum Type { snapshot, history } private String datasetPath; @CommandLine.Option( - names = {"--help", "-h"}, + names = {"--exclude-historical-balance"}, + defaultValue = "false", + description = "only used with `operate=split -t snapshot`: when true, balance-trace " + + "and account-trace are excluded from the lite snapshot. " + + "Default: ${DEFAULT-VALUE} (legacy behavior; trace stores stay in the snapshot). " + + "This flag only has a functional impact when the source full node ran with " + + "`historyBalanceLookup=true` (off by default; most operators are unaffected). " + + "WARNING: when historyBalanceLookup was enabled, this loss is permanent: a lite " + + "node booted from such a snapshot cannot safely serve historical balance lookups " + + "(getBlockBalance may fail, and getAccountBalance may return balance=0 when " + + "account-trace data is missing). Running merge afterwards will NOT restore the " + + "feature. If you need to keep historyBalanceLookup working on the resulting " + + "lite node, do NOT enable this flag. `split -t history` and `merge` ignore " + + "this flag.", order = 5) + private boolean excludeHistoricalBalance; + + @CommandLine.Option( + names = {"--help", "-h"}, + order = 6) private boolean help; @@ -120,6 +145,7 @@ public Integer call() { switch (this.operate) { case split: if (Type.snapshot == this.type) { + warnIfExcludingHistoricalBalance(); generateSnapshot(fnDataPath, datasetPath); } else if (Type.history == type) { generateHistory(fnDataPath, datasetPath); @@ -253,12 +279,52 @@ public void completeHistoryData(String historyDir, String liteDir) { spec.commandLine().getOut().format("Merge history finished, take %d s.", during).println(); } + /** + * Compute the directories to exclude from the lite snapshot. + *

+ * Default ({@code --exclude-historical-balance=false}): the legacy archive set + * (5 dbs); {@link #BALANCE_TRACE_DB_NAME} / {@link #ACCOUNT_TRACE_DB_NAME} + * stay with the snapshot as state-style stores. + *

+ * Opt-in ({@code --exclude-historical-balance=true}): the trace stores are + * additionally excluded, producing a smaller lite snapshot at the cost of + * dropping historical balance lookup support on the resulting lite node. + * Only {@code split -t snapshot} consults this. {@code split -t history} + * and {@code merge} always use the legacy archive set. + */ + private List snapshotExclusion() { + if (!excludeHistoricalBalance) { + return archiveDbs; + } + return Stream.concat(archiveDbs.stream(), traceDbs.stream()) + .collect(Collectors.toList()); + } + + private void warnIfExcludingHistoricalBalance() { + if (!excludeHistoricalBalance) { + return; + } + String msg = "WARNING: --exclude-historical-balance is enabled. balance-trace / account-trace " + + "will be excluded from the lite snapshot. This only matters when the source full " + + "node ran with historyBalanceLookup=true (off by default; most operators are " + + "unaffected). When that switch was enabled, this loss is permanent: lite nodes " + + "booted from this snapshot cannot safely serve historical balance lookups " + + "(getBlockBalance may fail, and getAccountBalance may return balance=0 when " + + "account-trace data is missing). Running merge afterwards will NOT restore the " + + "feature. If you need to keep historyBalanceLookup working on the resulting " + + "lite node, do NOT use this flag."; + logger.warn(msg); + spec.commandLine().getErr().println(spec.commandLine().getColorScheme() + .errorText(msg)); + } + private List getSnapshotDbs(String sourceDir) { List snapshotDbs = Lists.newArrayList(); File basePath = new File(sourceDir); + List excluded = snapshotExclusion(); Arrays.stream(Objects.requireNonNull(basePath.listFiles())) .filter(File::isDirectory) - .filter(dir -> !archiveDbs.contains(dir.getName())) + .filter(dir -> !excluded.contains(dir.getName())) .forEach(dir -> snapshotDbs.add(dir.getName())); return snapshotDbs; } @@ -723,4 +789,3 @@ public long getSnapshotMaxNum() { } - 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/main/resources/logback.xml b/plugins/src/main/resources/logback.xml index 6c415042e38..fa557f1a412 100644 --- a/plugins/src/main/resources/logback.xml +++ b/plugins/src/main/resources/logback.xml @@ -50,8 +50,10 @@ + + + - diff --git a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java index 60db838a80d..f7cb7b7f74f 100644 --- a/plugins/src/test/java/org/tron/plugins/DbLiteTest.java +++ b/plugins/src/test/java/org/tron/plugins/DbLiteTest.java @@ -1,11 +1,14 @@ package org.tron.plugins; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.tron.common.utils.PublicMethod.getRandomPrivateKey; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import lombok.extern.slf4j.Slf4j; import org.junit.After; @@ -68,7 +71,7 @@ public void shutdown() throws InterruptedException { context.close(); } - public void init(String dbType) throws IOException { + public void init(String dbType, boolean historyBalanceLookup) throws IOException { dbPath = folder.newFolder().toString(); Args.setParam(new String[] { "-d", dbPath, "-w", "--p2p-disable", "true", "--storage-db-engine", dbType}, @@ -77,6 +80,7 @@ public void init(String dbType) throws IOException { Args.getInstance().setAllowAccountStateRoot(1); Args.getInstance().setRpcPort(PublicMethod.chooseRandomPort()); Args.getInstance().setRpcEnable(true); + Args.getInstance().setHistoryBalanceLookup(historyBalanceLookup); databaseDir = Args.getInstance().getStorage().getDbDirectory(); // init dbBackupConfig to avoid NPE Args.getInstance().dbBackupConfig = DbBackupConfig.getInstance(); @@ -89,11 +93,20 @@ 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", + testTools(dbType, checkpointVersion, false); + } + + public void testTools(String dbType, int checkpointVersion, boolean excludeHistoricalBalance) + throws InterruptedException, IOException { + logger.info("dbType {}, checkpointVersion {}, excludeHistoricalBalance {}", + dbType, checkpointVersion, excludeHistoricalBalance); + boolean historyBalanceLookup = excludeHistoricalBalance; + init(dbType, historyBalanceLookup); + final String[] argsForSnapshot = excludeHistoricalBalance + ? new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", + dbPath + File.separator + databaseDir, "--dataset-path", + dbPath, "--exclude-historical-balance"} + : new String[] {"-o", "split", "-t", "snapshot", "--fn-data-path", dbPath + File.separator + databaseDir, "--dataset-path", dbPath}; final String[] argsForHistory = @@ -115,6 +128,16 @@ public void testTools(String dbType, int checkpointVersion) FileUtil.deleteDir(Paths.get(dbPath, databaseDir, "trans-cache").toFile()); // generate snapshot cli.execute(argsForSnapshot); + Path snapshotDir = Paths.get(dbPath, "snapshot"); + if (excludeHistoricalBalance) { + // when --exclude-historical-balance=true, the lite snapshot must not ship + // balance-trace / account-trace + assertFalse(snapshotDir.resolve("balance-trace").toFile().exists()); + assertFalse(snapshotDir.resolve("account-trace").toFile().exists()); + } else { + assertTrue(snapshotDir.resolve("balance-trace").toFile().exists()); + assertTrue(snapshotDir.resolve("account-trace").toFile().exists()); + } // start fullNode startApp(); // produce transactions @@ -166,7 +189,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/DbLiteExcludeHistoricalBalanceRocksDbTest.java b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java new file mode 100644 index 00000000000..766fe6d0924 --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/rocksdb/DbLiteExcludeHistoricalBalanceRocksDbTest.java @@ -0,0 +1,13 @@ +package org.tron.plugins.rocksdb; + +import java.io.IOException; +import org.junit.Test; +import org.tron.plugins.DbLiteTest; + +public class DbLiteExcludeHistoricalBalanceRocksDbTest extends DbLiteTest { + + @Test + public void testToolsWithExcludeHistoricalBalance() throws InterruptedException, IOException { + testTools("ROCKSDB", 1, true); + } +} 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/protocol/build.gradle b/protocol/build.gradle index 04d970b59db..0ce01a9bfb8 100644 --- a/protocol/build.gradle +++ b/protocol/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'com.google.protobuf' apply from: 'protoLint.gradle' def protobufVersion = '3.25.8' -def grpcVersion = '1.75.0' +// keep same version as protoc-gen-grpc-java for arm64 or macOS, see rootProject.archInfo.requires.ProtocGenVersion +def grpcVersion = '1.81.0' dependencies { api group: 'com.google.protobuf', name: 'protobuf-java', version: protobufVersion 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). diff --git a/sprout-verifying.key b/sprout-verifying.key deleted file mode 100644 index 16655abb5d7..00000000000 Binary files a/sprout-verifying.key and /dev/null differ