From cf6aa0ef8c707618f40515a808d277a9e1dca62d Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 10:20:27 +0800 Subject: [PATCH 1/6] fix(plugins): exclude transitive leveldbjni-all from :crypto Toolkit.jar built on x86_64 throws NoSuchMethodError on org.iq80.leveldb.Options.maxBatchSize(I) at runtime when running `db archive` / `--rebuild-manifest`. Root cause: the :crypto module added in PR #6637 brings in :common -> :platform transitively, which pulls in org.fusesource.leveldbjni:leveldbjni-all:1.8. The direct :platform dependency on x86 already excludes leveldbjni-all (so the io.github.tronprotocol fork at 1.18.2 can be used instead), but the transitive path through :crypto does not. Both jars contain org/iq80/leveldb/Options.class; in the fat jar the duplicate-entry write order leaves the 1.8 copy, which lacks Options.maxBatchSize(int). Why tests didn't catch it: - :plugins:test classpath has both jars side-by-side; the JVM classloader returns the first match, which is 1.18.2 (declared directly), so tests pass. - The fat jar binaryRelease task uses zipTree to merge classes; duplicate entries are overwritten by later writes, so the surviving Options.class can be the 1.8 copy. Same classpath, opposite winner. - ARM64 path declares only :platform directly (no 1.18.2), so there is no conflict; Archive tests are also excluded on ARM64. Fix: exclude org.fusesource.leveldbjni:leveldbjni-all from the :crypto implementation, mirroring the existing exclusion on the direct :platform dependency. After this change the x86 runtimeClasspath contains only io.github.tronprotocol:leveldbjni-all:1.18.2, and the fat jar's Options.class always exposes maxBatchSize(int). Verified on x86_64 + JDK 8 (eclipse-temurin:8-jdk container): - :plugins:dependencies no longer lists leveldbjni-all 1.8 - java -jar Toolkit.jar db archive -d exits 0 with the expected "directory does not contain any database" message - :plugins:test passes --- plugins/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/build.gradle b/plugins/build.gradle index 2e358a884a..29189d814d 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,6 +39,12 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' + // :crypto -> :common -> :platform transitively pulls leveldbjni-all 1.8, + // which conflicts with the io.github.tronprotocol leveldbjni-all 1.18.2 + // declared below on x86. Both jars contain org/iq80/leveldb/Options.class; + // in the fat jar the duplicate-entry write order can leave the 1.8 copy, + // which lacks Options.maxBatchSize(int) and breaks `db archive` at runtime. + 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' From 62dbcca969fe47e59a0b635347b1be0498063412 Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 10:20:55 +0800 Subject: [PATCH 2/6] ci(plugins): add Toolkit jar smoke tests to platform builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After `./gradlew clean build` finishes, run the freshly built Toolkit.jar through a few picocli subcommands so that runtime class-loading errors in the fat jar are caught by CI rather than slipping into a release. Without this step, dependency-conflict bugs that survive `:plugins:test` can still ship in the fat jar, because: - the test classpath keeps source jars separate (classloader returns the first match), but - the fat jar merges class entries with last-write-wins, so a different copy of a duplicated class can end up in the artifact. The recent NoSuchMethodError on Options.maxBatchSize is exactly this shape of bug: tests passed, fat jar broke. The smoke test catches it. Smoke commands run on every platform build (macos / ubuntu-arm / rockylinux-x86 / debian11-x86): - `java -jar Toolkit.jar help` — exercises the top-level command tree - `java -jar Toolkit.jar db --help` — loads all db subcommands - `java -jar Toolkit.jar db archive -h` — directly exercises the leveldb Options path that previously broke - `java -jar Toolkit.jar keystore --help` — exercises the :crypto module path added by the recent keystore migration Each step adds ~10-15s; jobs run in parallel so PR critical path increases by at most ~15s. --- .github/workflows/pr-build.yml | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 249bcaf28f..dd005f98b7 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 From 69d7f215693d9cfa34a6cc6438b729db64c7ce1d Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 16:11:06 +0800 Subject: [PATCH 3/6] fix(plugins): align :crypto excludes with direct :platform dependency Per review on PR #6738: the direct :platform dependency on x86 already excludes leveldbjni-all, zksnark-java-sdk, and commons-io, but the transitive :crypto -> :common -> :platform path was only excluding leveldbjni-all. Verified via :plugins:dependencyInsight that zksnark-java-sdk and commons-io were leaking back into x86 runtimeClasspath through :crypto. Mirror the remaining two excludes on :crypto so both paths to :platform agree on what is kept out of the plugins fat jar. This preserves the intent of the existing :platform excludes rather than relaxing them. Re-verified on x86_64 + JDK 8: - runtimeClasspath no longer contains leveldbjni-all 1.8, zksnark-java-sdk, or commons-io:commons-io - java -jar Toolkit.jar db archive -d still exits 0 - :plugins:test, checkstyleMain, checkstyleTest all pass --- plugins/build.gradle | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/build.gradle b/plugins/build.gradle index 29189d814d..8b335e68be 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,12 +39,17 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' - // :crypto -> :common -> :platform transitively pulls leveldbjni-all 1.8, - // which conflicts with the io.github.tronprotocol leveldbjni-all 1.18.2 - // declared below on x86. Both jars contain org/iq80/leveldb/Options.class; - // in the fat jar the duplicate-entry write order can leave the 1.8 copy, - // which lacks Options.maxBatchSize(int) and breaks `db archive` at runtime. + // :crypto -> :common -> :platform transitively pulls in artifacts that + // the direct :platform dependency below already excludes on x86. Mirror + // those excludes here so both paths to :platform agree, otherwise the + // transitive copies leak into the fat jar. In particular leveldbjni-all + // 1.8 conflicts with io.github.tronprotocol leveldbjni-all 1.18.2: both + // jars carry org/iq80/leveldb/Options.class; the duplicate-entry write + // order in the fat jar can leave the 1.8 copy, which lacks + // Options.maxBatchSize(int) and breaks `db archive` at runtime. 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 group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' From 0aebaffb3db7ba551b054c3dbd4249cf367076f0 Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 16:31:30 +0800 Subject: [PATCH 4/6] fix(plugins): declare missing :crypto and :common jar deps for binaryRelease Per review on PR #6738 (halibobo1205): the binaryRelease Jar task zips up runtimeClasspath contents, which on this branch includes :crypto:jar and :common:jar (introduced by #6637), but the task only declared dependsOn on :protocol:jar and :platform:jar. Gradle was emitting an implicit_dependency warning and disabling execution optimizations to compensate. Reproduced locally: $ ./gradlew clean && ./gradlew :plugins:buildToolkitJar > Cannot expand ZIP '.../crypto/build/libs/crypto-1.0.0.jar' as it does not exist. Add :crypto:jar and :common:jar to dependsOn so partial / parallel / incremental builds can no longer race against missing dependency jars. Verified: - ./gradlew clean :plugins:buildToolkitJar passes with --warning-mode all and no implicit_dependency warning - Smoke (db archive -d ) still exits 0 on x86 docker - :plugins:test, checkstyleMain, checkstyleTest all pass --- plugins/build.gradle | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/build.gradle b/plugins/build.gradle index 8b335e68be..c4bb1ad891 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -141,7 +141,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) From 320da62c3d13dc99990213982e3006c410be372d Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 17:06:36 +0800 Subject: [PATCH 5/6] fix(plugins): revert zksnark/commons-io alignment, document why CI on rockylinux (x86_64 + JDK 8) failed under 0aebaffb3: org.tron.plugins.rocksdb.DbLiteRocksDbTest > testToolsWithRocksDB FAILED org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'zksnarkInitService' ... NoClassDefFoundError: org/apache/commons/io/FileUtils After commit 69d7f2156 mirrored the :platform excludes onto :crypto (reviewer's option A in #6738), x86 testRuntimeClasspath lost commons-io:commons-io and io.github.tronprotocol:zksnark-java-sdk entirely: both arrived only via :crypto -> :common -> :platform, which is now also excluding them. ARM64 was unaffected because the ARM64 branch declares :platform without exclusions. Diagnosis: the DbLiteTest Spring boot path loads org.tron.core.zen.ZksnarkInitService, which references both org.apache.commons.io.FileUtils and org.tron.common.zksnark.LibrustzcashWrapper at runtime. Reproduced on x86_64 docker: - removing only commons-io exclude -> FAILED at LibrustzcashWrapper - removing both exclusions -> testToolsWithRocksDB PASSED The :platform-side excludes for these two artifacts are therefore deduplication only, not a "kept out of plugins" intent. The leveldbjni-all exclude is the only one that must be mirrored, because that one is the actual classpath conflict. This commit drops the zksnark-java-sdk and commons-io excludes from :crypto and adds a comment recording why they are intentionally asymmetric with :platform. Verified on x86_64 + JDK 8 (eclipse-temurin:8-jdk): - :plugins:test passes (was 4 failed under 0aebaffb3) - runtimeClasspath still does NOT contain leveldbjni-all 1.8 - runtimeClasspath now contains commons-io and zksnark-java-sdk - Toolkit.jar smoke (db archive -d ) still exits 0 --- plugins/build.gradle | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/build.gradle b/plugins/build.gradle index c4bb1ad891..bbbb26e540 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,17 +39,20 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' - // :crypto -> :common -> :platform transitively pulls in artifacts that - // the direct :platform dependency below already excludes on x86. Mirror - // those excludes here so both paths to :platform agree, otherwise the - // transitive copies leak into the fat jar. In particular leveldbjni-all - // 1.8 conflicts with io.github.tronprotocol leveldbjni-all 1.18.2: both - // jars carry org/iq80/leveldb/Options.class; the duplicate-entry write - // order in the fat jar can leave the 1.8 copy, which lacks + // :crypto -> :common -> :platform transitively pulls leveldbjni-all + // 1.8, which conflicts with the io.github.tronprotocol fork + // leveldbjni-all 1.18.2 declared below on x86. Both jars carry + // org/iq80/leveldb/Options.class; the duplicate-entry write order in + // the fat jar can leave the 1.8 copy, which lacks // Options.maxBatchSize(int) and breaks `db archive` at runtime. + // + // The direct :platform dependency below also excludes + // zksnark-java-sdk and commons-io, but those are *not* mirrored here: + // the plugins test runtime (DbLiteTest -> Spring -> ZksnarkInitService) + // actually loads classes from both, so :crypto must keep transitively + // providing them. The :platform-side excludes are deduplication, not + // a "kept out of plugins" intent. 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 group: 'info.picocli', name: 'picocli', version: '4.6.3' implementation group: 'com.typesafe', name: 'config', version: '1.3.2' From 26f463b72255e07cdd087cc0c4918805356f8230 Mon Sep 17 00:00:00 2001 From: Barbatos Date: Fri, 1 May 2026 22:56:21 +0800 Subject: [PATCH 6/6] refactor(plugins): scope :crypto leveldbjni-all exclude to x86 only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per follow-up review on PR #6738 (halibobo1205): the :platform-direct dependency in plugins/build.gradle is already split by arch (`if (rootProject.archInfo.isArm64) ... else ...`). The :crypto-side leveldbjni-all exclude is also only meaningful on x86 — on ARM64 the only leveldbjni-all on the runtime classpath comes via the direct :platform declaration at version 1.8, with no second copy to conflict with — so the unconditional exclude was a no-op on ARM64. Wrap the exclude in the same `if (!isArm64)` gate to match the existing arch pattern in this file and to make intent explicit (the exclusion is a fix for the x86-only fat-jar duplicate-class collision, not a defensive policy on every architecture). Behaviour-preserving on both arches: - ARM64: runtimeClasspath still has org.fusesource.leveldbjni:leveldbjni-all:1.8 via :platform direct (Gradle dedups the :crypto path); :plugins:test passes locally - x86: runtimeClasspath still has only io.github.tronprotocol:leveldbjni-all:1.18.2; :plugins:test passes and `db archive -d ` smoke exits 0 --- plugins/build.gradle | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/build.gradle b/plugins/build.gradle index bbbb26e540..edef0feb8f 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -39,20 +39,22 @@ dependencies { exclude group: 'io.prometheus' exclude group: 'org.aspectj' exclude group: 'org.apache.httpcomponents' - // :crypto -> :common -> :platform transitively pulls leveldbjni-all - // 1.8, which conflicts with the io.github.tronprotocol fork - // leveldbjni-all 1.18.2 declared below on x86. Both jars carry - // org/iq80/leveldb/Options.class; the duplicate-entry write order in - // the fat jar can leave the 1.8 copy, which lacks + // 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 duplicate-entry write order + // in the fat jar 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 + // its direct :platform declaration and no conflict. // - // The direct :platform dependency below also excludes - // zksnark-java-sdk and commons-io, but those are *not* mirrored here: - // the plugins test runtime (DbLiteTest -> Spring -> ZksnarkInitService) - // actually loads classes from both, so :crypto must keep transitively - // providing them. The :platform-side excludes are deduplication, not - // a "kept out of plugins" intent. - exclude group: 'org.fusesource.leveldbjni', module: 'leveldbjni-all' + // zksnark-java-sdk and commons-io are deliberately NOT mirrored from + // the :platform-direct excludes below: the plugins test runtime + // (DbLiteTest -> Spring -> ZksnarkInitService) loads classes from + // both at runtime, so :crypto must keep transitively providing them. + 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'