From 6aa43884fa4dc4400e630f3d68a60eb195648b56 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 9 Sep 2026 11:00:09 +0800 Subject: [PATCH 01/23] fix(keystore): fail fast on password stdin EOF instead of NPE inputPassword() now throws TronError(WITNESS_KEYSTORE_LOAD) when the password source closes before a line is available: the TTY branch null-checks Console.readPassword() (EOF raised a raw NullPointerException) and the non-TTY branch catches Scanner NoSuchElementException from a piped stdin EOF. Add piped-EOF test coverage; the TTY null-check is review-only (System.console() is always null under JUnit). --- .../java/org/tron/keystore/WalletUtils.java | 23 ++++++++++++++++++- .../WalletUtilsInputPasswordTest.java | 19 +++++++++++++++ .../KeystoreFactoryDeprecationTest.java | 11 +++++---- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crypto/src/main/java/org/tron/keystore/WalletUtils.java b/crypto/src/main/java/org/tron/keystore/WalletUtils.java index 2ce100823d9..00aed572be6 100644 --- a/crypto/src/main/java/org/tron/keystore/WalletUtils.java +++ b/crypto/src/main/java/org/tron/keystore/WalletUtils.java @@ -19,12 +19,14 @@ import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.EnumSet; +import java.util.NoSuchElementException; 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; +import org.tron.core.exception.TronError; /** * Utility functions for working with Wallet files. @@ -233,6 +235,14 @@ public static String inputPassword() { while (true) { if (cons != null) { char[] pwd = cons.readPassword("password: "); + // Console EOF (e.g. Ctrl+D) returns null; fail fast instead of + // crashing with a raw NullPointerException. + if (pwd == null) { + throw new TronError( + "password input closed (EOF) while reading from the console; " + + "restart with --password or provide an interactive TTY", + TronError.ErrCode.WITNESS_KEYSTORE_LOAD); + } password = String.valueOf(pwd); } else { // Preserve the full password including embedded whitespace. @@ -241,7 +251,18 @@ public static String inputPassword() { // 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()); + String line; + try { + line = in.nextLine(); + } catch (NoSuchElementException e) { + // Piped stdin reached EOF before a password line was available; + // fail fast instead of leaking a raw NoSuchElementException. + throw new TronError( + "password input closed (EOF): piped stdin provided no password " + + "line; restart with --password or provide an interactive TTY", + e, TronError.ErrCode.WITNESS_KEYSTORE_LOAD); + } + password = stripPasswordLine(line); } if (passwordValid(password)) { return password; diff --git a/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java index 64752b9ca49..fc43331fc18 100644 --- a/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java +++ b/framework/src/test/java/org/tron/keystore/WalletUtilsInputPasswordTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -9,6 +10,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.tron.core.exception.TronError; /** * Verifies that {@link WalletUtils#inputPassword()} preserves the full @@ -95,6 +97,23 @@ public void testInputPasswordPreservesLeadingAndTrailingSpaces() { " with spaces ", pw); } + // ---------- EOF fail-fast tests ---------- + + @Test(timeout = 5000) + public void testInputPasswordPipedStdinEofThrowsTronError() { + // Non-TTY branch: Scanner.nextLine() throws NoSuchElementException at + // EOF; it must be translated into a fail-fast TronError. + // (The TTY branch's Console.readPassword() null-on-EOF check cannot be + // exercised under JUnit, where System.console() is always null and + // java.io.Console is final — covered by code review only.) + System.setIn(new ByteArrayInputStream(new byte[0])); + + TronError err = assertThrows(TronError.class, WalletUtils::inputPassword); + + assertEquals("Piped stdin EOF must map to the keystore-load error code", + TronError.ErrCode.WITNESS_KEYSTORE_LOAD, err.getErrCode()); + } + @Test(timeout = 10000) public void testInputPassword2TwicePipedPreservesInternalWhitespace() { // M1: verifies the double-read path (inputPassword2Twice → inputPassword() diff --git a/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java index 860980d21e5..7c77b734ac0 100644 --- a/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java +++ b/framework/src/test/java/org/tron/program/KeystoreFactoryDeprecationTest.java @@ -1,5 +1,6 @@ package org.tron.program; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; @@ -12,6 +13,7 @@ import org.junit.Test; import org.tron.common.TestConstants; import org.tron.core.config.args.Args; +import org.tron.core.exception.TronError; /** * Verifies the deprecated --keystore-factory CLI. @@ -115,19 +117,18 @@ public void testQuitCommand() throws Exception { @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. + // genkeystore reads the password via WalletUtils.inputPassword; the + // scripted stdin hits EOF on the second read, so the prompt now throws + // TronError (KEY-01 fix) instead of being caught by the REPL loop. ByteArrayOutputStream outContent = new ByteArrayOutputStream(); System.setOut(new PrintStream(outContent)); System.setIn(new ByteArrayInputStream("genkeystore\nexit\n".getBytes())); - KeystoreFactory.start(); + assertThrows(TronError.class, 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) From 2a4950c674d4d13cc81a57316564e1a83de3b3ff Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 9 Sep 2026 11:23:32 +0800 Subject: [PATCH 02/23] fix(keystore): validate KDF parameter bounds before decrypt Untrusted keystore KDF cost parameters previously drove Bouncy Castle into NegativeArraySizeException (huge scrypt n), a ~4GiB allocation/OOM (n=2^22,r=8), ArithmeticException (r*n overflow), or a multi-hour pbkdf2 CPU hang. validationError() now enforces bounds as the single chokepoint shared by validate() and isValidKeystoreFile(): scrypt n power-of-2 in [2^12,2^20], r<=8, p<=8, 128*r*n<=1GiB (long arithmetic), dklen<=128; pbkdf2 c<=2^20, dklen<=128; missing/malformed kdfparams rejected. decrypt() additionally wraps both KDF invocations so residual RuntimeExceptions surface as CipherException (Errors stay fatal). Add hostile-keystore and boundary-valid test coverage. --- .../main/java/org/tron/keystore/Wallet.java | 96 ++++++- .../keystore/WalletDecryptBoundsTest.java | 258 ++++++++++++++++++ 2 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java diff --git a/crypto/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java index d63525b1e4d..7218e817e4d 100644 --- a/crypto/src/main/java/org/tron/keystore/Wallet.java +++ b/crypto/src/main/java/org/tron/keystore/Wallet.java @@ -63,6 +63,21 @@ public class Wallet { private static final int CURRENT_VERSION = 3; private static final String CIPHER = "aes-128-ctr"; + // Upper bounds for untrusted keystore KDF cost parameters. A hostile + // keystore file may declare arbitrary KDF strengths: extreme scrypt + // parameters make Bouncy Castle either allocate unbounded memory + // (n*r*128 bytes — e.g. n=2^22, r=8 requests 4 GiB and dies with OOM) + // or compute for hours, and out-of-range values can crash the JVM with + // NegativeArraySizeException / ArithmeticException inside SCrypt.generate. + // All values here are rejected by validationError() before any KDF runs. + private static final int SCRYPT_N_MIN = 1 << 12; + private static final int SCRYPT_N_MAX = 1 << 20; + private static final int SCRYPT_R_MAX = 8; + private static final int SCRYPT_P_MAX = 8; + private static final long SCRYPT_MEMORY_MAX_BYTES = 1L << 30; // 128 * r * n cap = 1 GiB + private static final int KDF_DKLEN_MAX = 128; + private static final int PBKDF2_C_MAX = 1 << 20; + public static WalletFile create(String password, SignInterface sign, int n, int p) throws CipherException { @@ -194,7 +209,16 @@ public static SignInterface decrypt(String password, WalletFile walletFile, int p = scryptKdfParams.getP(); int r = scryptKdfParams.getR(); byte[] salt = ByteArray.fromHexString(scryptKdfParams.getSalt()); - derivedKey = generateDerivedScryptKey(password.getBytes(UTF_8), salt, n, r, p, dklen); + try { + derivedKey = generateDerivedScryptKey(password.getBytes(UTF_8), salt, n, r, p, dklen); + } catch (RuntimeException e) { + // Defense-in-depth: any residual KDF engine failure (e.g. an + // IllegalArgumentException from Bouncy Castle) must surface as a + // clean CipherException, never as a raw RuntimeException stack. + // Errors (OutOfMemoryError etc.) are deliberately not caught — + // they remain fatal. + throw new CipherException("Scrypt key derivation failed", e); + } } else if (kdfParams instanceof WalletFile.Aes128CtrKdfParams) { WalletFile.Aes128CtrKdfParams aes128CtrKdfParams = (WalletFile.Aes128CtrKdfParams) crypto.getKdfparams(); @@ -202,7 +226,11 @@ public static SignInterface decrypt(String password, WalletFile walletFile, String prf = aes128CtrKdfParams.getPrf(); byte[] salt = ByteArray.fromHexString(aes128CtrKdfParams.getSalt()); - derivedKey = generateAes128CtrDerivedKey(password.getBytes(UTF_8), salt, c, prf); + try { + derivedKey = generateAes128CtrDerivedKey(password.getBytes(UTF_8), salt, c, prf); + } catch (RuntimeException e) { + throw new CipherException("Pbkdf2 key derivation failed", e); + } } else { throw new CipherException("Unable to deserialize params: " + crypto.getKdf()); } @@ -237,11 +265,15 @@ public static SignInterface decrypt(String password, WalletFile walletFile, /** * 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). + * V3 keystore shape (current version, known cipher, known KDF, and KDF + * cost parameters within safe bounds). * - *

Shared by {@link #validate(WalletFile)} (which throws the message) - * and {@link #isValidKeystoreFile(WalletFile)} (which returns boolean - * for discovery-style filtering). + *

KDF cost bounds are enforced here — the single chokepoint shared by + * {@link #validate(WalletFile)} (which throws the message) and + * {@link #isValidKeystoreFile(WalletFile)} (which returns boolean for + * discovery-style filtering) — so untrusted keystore files can never + * drive the KDF into unbounded memory allocation, multi-hour CPU + * exhaustion, or engine-level crashes before {@link #decrypt} runs. */ private static String validationError(WalletFile walletFile) { if (walletFile.getVersion() != CURRENT_VERSION) { @@ -259,6 +291,58 @@ private static String validationError(WalletFile walletFile) { if (kdf == null || (!kdf.equals(PBKDF2) && !kdf.equals(SCRYPT))) { return "KDF type is not supported"; } + WalletFile.KdfParams kdfParams = crypto.getKdfparams(); + if (kdfParams == null) { + // Structural stubs (e.g. tooling fixtures) may omit kdfparams entirely; + // there are no cost parameters to bound. Real keystores always carry them. + return null; + } + if (SCRYPT.equals(kdf)) { + if (!(kdfParams instanceof WalletFile.ScryptKdfParams)) { + return "Scrypt KDF params are missing or malformed"; + } + WalletFile.ScryptKdfParams params = (WalletFile.ScryptKdfParams) kdfParams; + return scryptKdfParamsError(params.getDklen(), params.getN(), params.getR(), params.getP()); + } + if (!(kdfParams instanceof WalletFile.Aes128CtrKdfParams)) { + return "Pbkdf2 KDF params are missing or malformed"; + } + WalletFile.Aes128CtrKdfParams params = (WalletFile.Aes128CtrKdfParams) kdfParams; + return pbkdf2KdfParamsError(params.getDklen(), params.getC()); + } + + private static String scryptKdfParamsError(int dklen, int n, int r, int p) { + // dklen must yield at least 32 bytes: generateMac copies derivedKey[16..32) + // and AES-128 needs bytes [0..16); smaller keys throw raw ArrayIndexOutOfBoundsException. + if (dklen < 32 || dklen > KDF_DKLEN_MAX) { + return "Scrypt dklen is out of range [32, " + KDF_DKLEN_MAX + "]: " + dklen; + } + if (n < SCRYPT_N_MIN || n > SCRYPT_N_MAX || (n & (n - 1)) != 0) { + return "Scrypt n must be a power of 2 in [" + SCRYPT_N_MIN + ", " + SCRYPT_N_MAX + + "]: " + n; + } + if (r < 1 || r > SCRYPT_R_MAX) { + return "Scrypt r is out of range [1, " + SCRYPT_R_MAX + "]: " + r; + } + if (p < 1 || p > SCRYPT_P_MAX) { + return "Scrypt p is out of range [1, " + SCRYPT_P_MAX + "]: " + p; + } + // Compute in long arithmetic: hostile r * n must not overflow int. + long memoryBytes = 128L * r * n; + if (memoryBytes > SCRYPT_MEMORY_MAX_BYTES) { + return "Scrypt memory requirement 128*r*n=" + memoryBytes + + " exceeds the " + SCRYPT_MEMORY_MAX_BYTES + " byte limit"; + } + return null; + } + + private static String pbkdf2KdfParamsError(int dklen, int c) { + if (dklen < 1 || dklen > KDF_DKLEN_MAX) { + return "Pbkdf2 dklen is out of range [1, " + KDF_DKLEN_MAX + "]: " + dklen; + } + if (c < 1 || c > PBKDF2_C_MAX) { + return "Pbkdf2 iteration count c is out of range [1, " + PBKDF2_C_MAX + "]: " + c; + } return null; } diff --git a/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java new file mode 100644 index 00000000000..353d7732b05 --- /dev/null +++ b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java @@ -0,0 +1,258 @@ +package org.tron.keystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.tron.core.exception.CipherException; + +/** + * Verifies that untrusted keystore KDF cost parameters are bounded before + * any key-derivation work happens. A hostile keystore previously drove + * Bouncy Castle into {@code NegativeArraySizeException} (huge scrypt n), + * a ~4 GiB allocation / OOM (n=2^22, r=8), {@code ArithmeticException} + * (r*n overflow), or a multi-hour pbkdf2 CPU hang. All of these must now + * be rejected by {@link Wallet#validate(WalletFile)} (which is called at + * the top of {@link Wallet#decrypt}), never reaching the KDF engine. + * + *

All rejection tests are fast by construction: validation performs no + * KDF execution, and no test ever invokes decrypt with scrypt parameters + * that pass the bounds checks (except the deliberately corrupted + * low-cost keystore used for the CipherException wrap test). + */ +public class WalletDecryptBoundsTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private static final String SCRYPT_TEMPLATE = + "{\"version\":3,\"address\":\"addr\",\"id\":\"test\",\"crypto\":{" + + "\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"00\"," + + "\"cipherparams\":{\"iv\":\"00\"},\"mac\":\"00\",\"kdf\":\"scrypt\"," + + "\"kdfparams\":{\"dklen\":%d,\"n\":%d,\"r\":%d,\"p\":%d,\"salt\":\"00\"}}}"; + + private static final String PBKDF2_TEMPLATE = + "{\"version\":3,\"address\":\"addr\",\"id\":\"test\",\"crypto\":{" + + "\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"00\"," + + "\"cipherparams\":{\"iv\":\"00\"},\"mac\":\"00\",\"kdf\":\"pbkdf2\"," + + "\"kdfparams\":{\"dklen\":%d,\"c\":%d,\"prf\":\"hmac-sha256\",\"salt\":\"00\"}}}"; + + private static WalletFile parseKeystore(String json) throws Exception { + return OBJECT_MAPPER.readValue(json, WalletFile.class); + } + + private static CipherException validateFailure(String json) throws Exception { + WalletFile walletFile = parseKeystore(json); + return assertThrows(CipherException.class, () -> Wallet.validate(walletFile)); + } + + // ---------- hostile scrypt n ---------- + + @Test + public void testValidateRejectsScryptNNegativeArrayCase() throws Exception { + // n=2^23 previously crashed inside SCrypt.generate with + // NegativeArraySizeException; must be rejected before the KDF runs. + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 23, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("n")); + } + + @Test + public void testValidateRejectsScryptNOomCase() throws Exception { + // n=2^22, r=8 requests 128*r*n = 4 GiB and previously died with OOM; + // validation must reject it (fast, no allocation). + long start = System.nanoTime(); + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 22, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("n")); + // Rejection is pure validation: must be far cheaper than any KDF run. + assertTrue("rejection must happen before allocation", + System.nanoTime() - start < 5_000_000_000L); + } + + @Test + public void testValidateRejectsScryptNBelowFloor() throws Exception { + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 11, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt n")); + } + + @Test + public void testValidateRejectsScryptNAboveCap() throws Exception { + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 21, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt n")); + } + + @Test + public void testValidateRejectsScryptNNotPowerOfTwo() throws Exception { + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 12345, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt n")); + } + + // ---------- hostile scrypt r / p / dklen ---------- + + @Test + public void testValidateRejectsScryptRArithmeticCase() throws Exception { + // r=2^24 with n*r overflow previously crashed inside + // SCrypt.generate with ArithmeticException. + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 13, 1 << 24, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt r")); + } + + @Test + public void testValidateRejectsScryptPAboveCap() throws Exception { + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 32, 1 << 13, 8, 9)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt p")); + } + + @Test + public void testValidateRejectsOversizedDklen() throws Exception { + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 4096, 1 << 13, 8, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt dklen")); + } + + @Test + public void testValidateRejectsScryptDklenBelowMacFloor() throws Exception { + // dklen=16 passes the old < 1 sanity floor but is too short for + // generateMac (needs derivedKey[16..32)) — validation must reject it + // before any KDF runs, otherwise a raw ArrayIndexOutOfBoundsException + // escapes decrypt(). + CipherException err = validateFailure(String.format(SCRYPT_TEMPLATE, + 16, 1 << 12, 1, 1)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt dklen")); + assertTrue(err.getMessage(), err.getMessage().contains("[32")); + } + + // ---------- missing kdfparams ---------- + // + // Note: a keystore JSON that omits "kdfparams" entirely fails at + // deserialization (Jackson external type id), which is already a safe + // rejection. The validation chokepoint itself is exercised here via + // programmatically built WalletFile objects with a null kdfparams. + + private static WalletFile walletWithKdfparams( + String kdf, WalletFile.KdfParams kdfParams) { + WalletFile walletFile = new WalletFile(); + walletFile.setVersion(3); + walletFile.setAddress("addr"); + WalletFile.Crypto crypto = new WalletFile.Crypto(); + crypto.setCipher("aes-128-ctr"); + crypto.setCiphertext("00"); + WalletFile.CipherParams cipherParams = new WalletFile.CipherParams(); + cipherParams.setIv("00"); + crypto.setCipherparams(cipherParams); + crypto.setMac("00"); + crypto.setKdf(kdf); + crypto.setKdfparams(kdfParams); + walletFile.setCrypto(crypto); + return walletFile; + } + + @Test + public void testValidateToleratesMissingScryptKdfparams() { + // Structural stubs without kdfparams are tolerated (KeystoreCliUtilsTest + // fixture contract); decrypt() still fails safely because the + // ClassCastException (RuntimeException) is wrapped into CipherException. + WalletFile walletFile = walletWithKdfparams("scrypt", null); + try { + Wallet.validate(walletFile); + } catch (CipherException e) { + fail("missing kdfparams must be tolerated, got: " + e.getMessage()); + } + assertThrows(CipherException.class, + () -> Wallet.decrypt("password123", walletFile, true)); + } + + @Test + public void testValidateToleratesMissingPbkdf2Kdfparams() { + WalletFile walletFile = walletWithKdfparams("pbkdf2", null); + try { + Wallet.validate(walletFile); + } catch (CipherException e) { + fail("missing kdfparams must be tolerated, got: " + e.getMessage()); + } + assertThrows(CipherException.class, + () -> Wallet.decrypt("password123", walletFile, true)); + } + + // ---------- hostile pbkdf2 ---------- + + @Test + public void testValidateRejectsPbkdf2MaxIterations() throws Exception { + // c=2147483647 is a multi-hour CPU hang; must be rejected up front. + CipherException err = validateFailure(String.format(PBKDF2_TEMPLATE, + 32, Integer.MAX_VALUE)); + assertTrue(err.getMessage(), err.getMessage().contains("Pbkdf2 iteration count")); + } + + @Test + public void testValidateRejectsPbkdf2OversizedDklen() throws Exception { + CipherException err = validateFailure(String.format(PBKDF2_TEMPLATE, + 4096, 262144)); + assertTrue(err.getMessage(), err.getMessage().contains("Pbkdf2 dklen")); + } + + // ---------- behavior freeze: legitimate keystores still pass ---------- + + @Test + public void testValidateAcceptsStandardDefaults() throws Exception { + // Wallet.createStandard: n=2^18, r=8, p=1, dklen=32. + WalletFile standard = parseKeystore(String.format(SCRYPT_TEMPLATE, + 32, 1 << 18, 8, 1)); + Wallet.validate(standard); + assertTrue(Wallet.isValidKeystoreFile(standard)); + + // Wallet.createLight: n=2^12, r=8, p=6, dklen=32. + WalletFile light = parseKeystore(String.format(SCRYPT_TEMPLATE, + 32, 1 << 12, 8, 6)); + Wallet.validate(light); + assertTrue(Wallet.isValidKeystoreFile(light)); + } + + @Test + public void testValidateAcceptsUpperBoundaryParams() throws Exception { + // n=2^20, r=8, p=1 sits exactly at the cap (128*r*n = 1 GiB) and is a + // sane (if heavy) generator choice: validation must accept it. The + // test only asserts validation — the actual scrypt run is never + // executed here. + WalletFile boundary = parseKeystore(String.format(SCRYPT_TEMPLATE, + 128, 1 << 20, 8, 1)); + Wallet.validate(boundary); + assertTrue(Wallet.isValidKeystoreFile(boundary)); + } + + @Test + public void testValidateAcceptsPbkdf2Defaults() throws Exception { + WalletFile walletFile = parseKeystore(String.format(PBKDF2_TEMPLATE, + 32, 262144)); + Wallet.validate(walletFile); + assertTrue(Wallet.isValidKeystoreFile(walletFile)); + } + + // ---------- decrypt wrap: in-bounds but corrupted keystore ---------- + + @Test + public void testDecryptCorruptedKeystoreThrowsCipherException() throws Exception { + // In-bounds low-cost scrypt params (fast), but tampered MAC: + // decrypt must fail with a clean CipherException, never leak a raw + // RuntimeException stack. + String json = String.format(SCRYPT_TEMPLATE, 32, 1 << 12, 1, 1) + .replace("\"mac\":\"00\"", + "\"mac\":\"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\""); + WalletFile walletFile = parseKeystore(json); + Wallet.validate(walletFile); + + CipherException err = assertThrows(CipherException.class, + () -> Wallet.decrypt("password123", walletFile, true)); + assertEquals("Invalid password provided", err.getMessage()); + } +} From 6a7000ea3f8cfa8d1f51b912936dd61dd09a2e9b Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 9 Sep 2026 11:34:08 +0800 Subject: [PATCH 03/23] feat(cli): deprecate --private-key/--password in favor of localwitnesskeystore Passing key material on the command line (-p/--private-key, --password) exposes it in the process list and shell history, and plaintext keys in the localwitness config option are readable by anyone with access to the config file. Mark both CLI flags @Deprecated (scheduled for removal in a future release, non-breaking for now) and log actionable warnings when these plain-key paths are used at witness startup, pointing users to the encrypted localwitnesskeystore. Update README and configuration docs accordingly. --- README.md | 3 +++ docs/configuration.md | 5 +++-- .../src/main/java/org/tron/core/config/args/Args.java | 10 ++++++++++ .../java/org/tron/core/config/args/CLIParameter.java | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edf99c4df92..63bce4963f3 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,9 @@ Fill in the private key of your SR account into the `localwitness` list in the c ] ``` + +> **Note**: Plain private keys in `localwitness` (and the `-p/--private-key` CLI flag) are deprecated and insecure — they are readable by anyone with access to the config file, process list, or shell history. Use the encrypted [`localwitnesskeystore`](https://tronprotocol.github.io/documentation-en/using_javatron/toolkit/) instead. + Check [Starting a Block Production Node](https://tronprotocol.github.io/documentation-en/using_javatron/installing_javatron/#starting-a-block-production-node) for more details. You could also test the process by connecting to a testnet or setting up a private network. diff --git a/docs/configuration.md b/docs/configuration.md index d021326a15e..6d28e6e63d6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,12 +151,13 @@ storage.properties = [ ### Block Production (Super Representatives) ```hocon -# Plain private key (use localwitnesskeystore for production) +# DEPRECATED: plain private key, insecure (anyone reading the config file +# sees the keys). Use localwitnesskeystore for production. localwitness = [ "your-private-key-hex" ] -# Recommended: keystore file +# Recommended: encrypted keystore file # localwitnesskeystore = [ # "/path/to/localwitnesskeystore.json" # ] 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 0bca242606e..ef741ef504f 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 @@ -905,6 +905,9 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { // path 1: CLI --private-key if (StringUtils.isNotBlank(cmd.privateKey)) { + logger.warn("Passing the witness private key via -p/--private-key is deprecated and " + + "insecure: the key is visible in the process list and shell history. Use " + + "localwitnesskeystore in the config file instead."); localWitnesses = WitnessInitializer.initFromCLIPrivateKey( cmd.privateKey, cmd.witnessAddress); return; @@ -914,6 +917,8 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { // path 2: config localwitness (private key list) if (!lwConfig.getPrivateKeys().isEmpty()) { + logger.warn("Storing plain witness private keys in 'localwitness' is insecure: any " + + "reader of the config file sees the keys. Use localwitnesskeystore instead."); localWitnesses = WitnessInitializer.initFromCFGPrivateKey( lwConfig.getPrivateKeys(), lwConfig.getAccountAddress()); return; @@ -921,6 +926,11 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { // path 3: config localwitnesskeystore + password if (!lwConfig.getKeystores().isEmpty()) { + if (StringUtils.isNotBlank(cmd.password)) { + logger.warn("Passing the keystore password via --password is insecure: it is visible " + + "in the process list and shell history. Omit --password to enter it " + + "interactively when prompted."); + } localWitnesses = WitnessInitializer.initFromKeystore( lwConfig.getKeystores(), cmd.password, lwConfig.getAccountAddress()); return; diff --git a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java index 4f056a32e3a..f3ffda69770 100644 --- a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java +++ b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java @@ -38,12 +38,14 @@ public class CLIParameter { @Parameter(names = {"-w", "--witness"}, description = "Is witness node") public boolean witness; + @Deprecated @Parameter(names = {"-p", "--private-key"}, description = "Witness private key") public String privateKey; @Parameter(names = {"--witness-address"}, description = "witness-address") public String witnessAddress; + @Deprecated @Parameter(names = {"--password"}, description = "password") public String password; From d8b8bd1f06c44185e5366db09f2736e5f1669b85 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 14 Sep 2026 17:29:58 +0800 Subject: [PATCH 04/23] fix(config): harden gRPC connection lifetime default and warn on half-configured RST pair --- .../org/tron/core/config/args/NodeConfig.java | 29 +++++++- .../tron/core/config/args/NodeConfigTest.java | 71 ++++++++++++++++++- .../org/tron/core/config/args/ArgsTest.java | 4 +- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 91945b5a73b..78b5c5af13e 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -208,6 +208,9 @@ public static class HttpConfig { public static class RpcConfig { public static final int DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION = 100; + // Secure built-in default for gRPC connection lifetime (idle & max age). + // Applied by postProcess() when the configured value is 0 (the default). + public static final long DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS = 60_000L; private boolean enable = true; private int port = 50051; @@ -373,11 +376,33 @@ private void postProcess() { rpc.maxConcurrentCallsPerConnection = RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION; } + // node.rpc.maxRstStream and node.rpc.secondsPerWindow only take effect + // together (RpcService applies RST limiting only when both are > 0). A + // half-configured pair would silently disable RST_STREAM flood + // protection — warn loudly so the misconfiguration is visible. + if (rpc.maxRstStream < 0 || rpc.secondsPerWindow < 0) { + throw new TronError("node.rpc.maxRstStream and node.rpc.secondsPerWindow " + + "must be non-negative, got: maxRstStream=" + rpc.maxRstStream + + ", secondsPerWindow=" + rpc.secondsPerWindow, PARAMETER_INIT); + } + if ((rpc.maxRstStream > 0 && rpc.secondsPerWindow <= 0) + || (rpc.maxRstStream <= 0 && rpc.secondsPerWindow > 0)) { + logger.warn("node.rpc.maxRstStream and node.rpc.secondsPerWindow only " + + "take effect together: exactly one of them is set while the other " + + "is 0, so RST_STREAM flood protection is DISABLED. Set both to " + + "positive values to enable it, or leave both at 0 (got: " + + "maxRstStream={}, secondsPerWindow={})", + rpc.maxRstStream, rpc.secondsPerWindow); + } + // 0 (the default) means "use the secure built-in default" (60s), NOT + // unlimited: the old 0 -> Long.MAX_VALUE conversion silently turned an + // unset or explicitly-zero value into an unbounded connection lifetime. + // Operators who want a longer lifetime must set an explicit positive value. if (rpc.maxConnectionIdleInMillis == 0) { - rpc.maxConnectionIdleInMillis = Long.MAX_VALUE; + rpc.maxConnectionIdleInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; } if (rpc.maxConnectionAgeInMillis == 0) { - rpc.maxConnectionAgeInMillis = Long.MAX_VALUE; + rpc.maxConnectionAgeInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; } // validateSignThreadNum: 0 = auto-detect diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index bcb8b09dd7a..8400662b8e6 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -107,8 +107,10 @@ public void testRpcDefaultsFromReference() { assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION, rpc.getMaxConcurrentCallsPerConnection()); assertEquals(1048576, rpc.getFlowControlWindow()); - assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis()); - assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis()); + // reference.conf keeps 0 (the default marker); postProcess converts 0 to + // the secure built-in 60s default, so the effective default is 60s. + assertEquals(60000L, rpc.getMaxConnectionIdleInMillis()); + assertEquals(60000L, rpc.getMaxConnectionAgeInMillis()); assertEquals(4194304, rpc.getMaxMessageSize()); assertEquals(8192, rpc.getMaxHeaderListSize()); assertEquals(1, rpc.getMinEffectiveConnection()); @@ -146,6 +148,71 @@ public void testRpcNegativeConcurrentCallsRejected() { "node.rpc.maxConcurrentCallsPerConnection must be non-negative, got: -1")); } + @Test + public void testRpcExplicitZeroIdleAndAgeFallsBackToSecureDefault() { + // Explicit 0 no longer means unlimited: postProcess converts 0 to the + // secure built-in 60s default. Operators wanting a longer lifetime must + // set an explicit positive value. + Config config = withRef( + "node { rpc { maxConnectionIdleInMillis = 0, maxConnectionAgeInMillis = 0 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS, + nc.getRpc().getMaxConnectionIdleInMillis()); + assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS, + nc.getRpc().getMaxConnectionAgeInMillis()); + } + + // ----- maxRstStream / secondsPerWindow: must be configured together ----- + + @Test + public void testRpcRstPairBothZeroOk() { + // Default: feature off. Must not throw. + NodeConfig nc = NodeConfig.fromConfig(withRef()); + assertEquals(0, nc.getRpc().getMaxRstStream()); + assertEquals(0, nc.getRpc().getSecondsPerWindow()); + } + + @Test + public void testRpcRstPairBothPositiveOk() { + Config config = withRef( + "node { rpc { maxRstStream = 1000, secondsPerWindow = 60 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(1000, nc.getRpc().getMaxRstStream()); + assertEquals(60, nc.getRpc().getSecondsPerWindow()); + } + + private static TronError rstError(String hocon) { + Config config = withRef("node { rpc { " + hocon + " } }"); + return assertThrows(TronError.class, () -> NodeConfig.fromConfig(config)); + } + + @Test + public void testRpcRstPairHalfConfiguredWarnsButStarts() { + // Only maxRstStream set: RST flood protection stays disabled, startup + // continues with a warning. + Config config = withRef("node { rpc { maxRstStream = 100 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertEquals(100, nc.getRpc().getMaxRstStream()); + assertEquals(0, nc.getRpc().getSecondsPerWindow()); + + // Only secondsPerWindow set: same trap, same warn-and-continue. + config = withRef("node { rpc { secondsPerWindow = 60 } }"); + nc = NodeConfig.fromConfig(config); + assertEquals(0, nc.getRpc().getMaxRstStream()); + assertEquals(60, nc.getRpc().getSecondsPerWindow()); + } + + @Test + public void testRpcRstNegativeValuesRejected() { + TronError exception = rstError("maxRstStream = -1, secondsPerWindow = 60"); + assertTrue(exception.getMessage().contains( + "must be non-negative, got: maxRstStream=-1")); + + exception = rstError("maxRstStream = 100, secondsPerWindow = -5"); + assertTrue(exception.getMessage().contains( + "must be non-negative, got: maxRstStream=100, secondsPerWindow=-5")); + } + @Test public void testRpcUserOverrideExplicitValues() { Config config = withRef( 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 36b8a3269c1..4001fc36bae 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 @@ -108,7 +108,9 @@ public void get() { .assertEquals(NettyServerBuilder .DEFAULT_FLOW_CONTROL_WINDOW, parameter.getFlowControlWindow()); Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); - Assert.assertEquals(Long.MAX_VALUE, parameter.getMaxConnectionAgeInMillis()); + // config-test.conf leaves maxConnectionAgeInMillis unset; postProcess + // converts the 0 default to the secure built-in 60s lifetime default. + Assert.assertEquals(60000L, 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()); From 6ec3d8aef3d0beaa7d44a910a86e41e2de6162cb Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 9 Sep 2026 12:12:26 +0800 Subject: [PATCH 05/23] build: pin Gradle distribution, enforce wrapper validation in CI, and tighten dependency sources - gradle-wrapper.properties: add official distributionSha256Sum for Gradle 7.6.4 bin zip (bed1da33cca0f557ab13691c77f38bb67388119e4794d113e051039b80af9bb1) - CI: run gradle/actions/wrapper-validation before any ./gradlew invocation in pr-build, pr-check, codeql, release-build, and integration-test workflows - build.gradle: move mavenLocal() after mavenCentral and drop the JitPack repository from both buildscript and subprojects blocks; all com.github.* dependencies (jsonrpc4j, java-semver, metrics-influxdb, software-and-algorithms) resolve from Maven Central - verification-metadata.xml: remove blanket SNAPSHOT trust rule and the javadoc/sources artifact trust rules; strict checksum verification now applies to every artifact --- .github/workflows/codeql.yml | 3 +++ .../workflows/integration-test-single-node.yml | 3 +++ .github/workflows/pr-build.yml | 15 +++++++++++++++ .github/workflows/pr-check.yml | 3 +++ .github/workflows/release-build.yml | 6 ++++++ build.gradle | 4 +--- gradle/verification-metadata.xml | 5 ----- gradle/wrapper/gradle-wrapper.properties | 1 + 8 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c3af93f787..f818cc5ad65 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,6 +34,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/integration-test-single-node.yml b/.github/workflows/integration-test-single-node.yml index 3c56843c799..3ce0cc248a3 100644 --- a/.github/workflows/integration-test-single-node.yml +++ b/.github/workflows/integration-test-single-node.yml @@ -25,6 +25,9 @@ jobs: - name: Checkout java-tron uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Set up JDK 8 uses: actions/setup-java@v5 with: diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index f35538c0961..bd38c3d8cd8 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -36,6 +36,9 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v5 with: @@ -72,6 +75,9 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -126,6 +132,9 @@ jobs: - name: Checkout code uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Check Java version run: java -version @@ -183,6 +192,9 @@ jobs: - name: Checkout code uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Check Java version run: java -version @@ -249,6 +261,9 @@ jobs: with: ref: ${{ github.event.pull_request.base.sha }} + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Cache Gradle packages uses: actions/cache@v5 with: diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 506a823a4f7..5ec401916d3 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -103,6 +103,9 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d67e2418bf6..c98747dadc8 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -54,6 +54,9 @@ jobs: with: ref: ${{ github.sha }} + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Cache Gradle packages uses: actions/cache@v5 with: @@ -121,6 +124,9 @@ jobs: with: ref: ${{ github.sha }} + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: Set up Temurin 17 uses: actions/setup-java@v5 with: diff --git a/build.gradle b/build.gradle index 65e72c0fb73..00d7db1405a 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,6 @@ subprojects { buildscript { repositories { mavenCentral() - maven { url 'https://jitpack.io' } maven { url "https://plugins.gradle.org/m2/" } @@ -84,10 +83,9 @@ subprojects { } repositories { - mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/plugins-release' } - maven { url 'https://jitpack.io' } + mavenLocal() } dependencies { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6a3e641d5d6..ada53b425c1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3,11 +3,6 @@ true false - - - - - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3994438e229..a2ccb4862b3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionSha256Sum=bed1da33cca0f557ab13691c77f38bb67388119e4794d113e051039b80af9bb1 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From fabe9a00ac4c0b49a3842978f027741898b580fd Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 9 Sep 2026 12:34:41 +0800 Subject: [PATCH 06/23] build(protocol): unify protoc-gen-grpc-java version and rebase docker image off CentOS 7 Use rootProject.grpcVersion (1.83.0) for protoc-gen-grpc-java on all architectures; the 1.60.0 pin only existed for the EOL CentOS 7 docker base (grpc-java#11371). Rebase the image onto rockylinux:8 (matching CI) and install JDK 8 via dnf (java-1.8.0-openjdk-devel) instead of the MD5-only verified Oracle 8u202 tarball; strip the devel package after the build so the runtime keeps only the JRE. Sync verification metadata: drop the 1.60.0 component, add protoc-gen-grpc-java-1.83.0-linux-x86_64.exe sha256 from Maven Central. --- build.gradle | 3 +-- docker/Dockerfile | 25 +++++-------------------- gradle/verification-metadata.xml | 11 +++-------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/build.gradle b/build.gradle index 00d7db1405a..6dd3bd95ab6 100644 --- a/build.gradle +++ b/build.gradle @@ -44,8 +44,7 @@ ext.archInfo = [ RocksdbVersion: isArm64 ? '9.7.4' : '5.15.10', // 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 ? rootProject.grpcVersion : '1.60.0' + ProtocGenVersion: rootProject.grpcVersion ], VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions" ] diff --git a/docker/Dockerfile b/docker/Dockerfile index 2732f5a55ed..97265016810 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,21 +1,10 @@ -FROM tronprotocol/centos7:0.2 +FROM rockylinux:8 ENV TMP_DIR="/tron-build" -ENV JDK_TAR="jdk-8u202-linux-x64.tar.gz" -ENV JDK_DIR="jdk1.8.0_202" -ENV JDK_MD5="0029351f7a946f6c05b582100c7d45b7" ENV BASE_DIR="/java-tron" - RUN set -o errexit -o nounset \ - && yum -y install git wget \ - && wget -P /usr/local https://github.com/frekele/oracle-java/releases/download/8u202-b08/$JDK_TAR \ - && echo "$JDK_MD5 /usr/local/$JDK_TAR" | md5sum -c \ - && tar -zxf /usr/local/$JDK_TAR -C /usr/local\ - && rm /usr/local/$JDK_TAR \ - && export JAVA_HOME=/usr/local/$JDK_DIR \ - && export CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar \ - && export PATH=$PATH:$JAVA_HOME/bin \ + && dnf -y install git wget unzip java-1.8.0-openjdk-devel \ && echo "git clone" \ && mkdir -p $TMP_DIR \ && cd $TMP_DIR \ @@ -24,19 +13,15 @@ RUN set -o errexit -o nounset \ && git checkout master \ && ./gradlew build -x test \ && cd build/distributions \ - && 7za x -y java-tron-1.0.0.zip \ + && unzip -q java-tron-1.0.0.zip \ && mv java-tron-1.0.0 $BASE_DIR \ && rm -rf $TMP_DIR \ && rm -rf ~/.gradle \ - && mv $JAVA_HOME/jre /usr/local \ - && rm -rf $JAVA_HOME \ - && yum clean all + && dnf -y remove java-1.8.0-openjdk-devel \ + && dnf clean all RUN wget -P $BASE_DIR/config https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/main_net_config.conf -ENV JAVA_HOME="/usr/local/jre" -ENV PATH=$PATH:$JAVA_HOME/bin - COPY docker-entrypoint.sh $BASE_DIR/bin WORKDIR $BASE_DIR diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ada53b425c1..d8bbb8f121a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1238,18 +1238,13 @@ - - - - - - - - + + + From f4f946ef08bd8550029d9661eb4d63ffc7c9ae89 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 14 Sep 2026 14:57:40 +0800 Subject: [PATCH 07/23] fix(docker): stop persisting full node command line to command.txt --- docker/docker-entrypoint.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index d3c5d4c65c8..5df57d1dd45 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -2,5 +2,4 @@ set -eo pipefail shopt -s nullglob -echo "./bin/FullNode $@" > command.txt exec "./bin/FullNode" "$@" \ No newline at end of file From 61294040421e16d31a816e492c9cb2b8e78ce8fe Mon Sep 17 00:00:00 2001 From: warku123 Date: Tue, 15 Sep 2026 16:37:50 +0800 Subject: [PATCH 08/23] fix(toolkit): enforce owner-only permissions on password and key files - readRegularFile now rejects files readable/writable by group or other on POSIX filesystems - covers --password-file (KeystoreNew/KeystoreImport/KeystoreUpdate) and --key-file (KeystoreImport) inputs - non-POSIX filesystems are skipped (view == null) --- .../org/tron/plugins/KeystoreCliUtils.java | 18 ++++++++ .../tron/plugins/KeystoreCliUtilsTest.java | 42 +++++++++++++++++++ .../org/tron/plugins/KeystoreImportTest.java | 42 +++++++++++++++++++ .../org/tron/plugins/KeystoreNewTest.java | 21 ++++++++++ .../org/tron/plugins/KeystoreUpdateTest.java | 32 ++++++++++++++ 5 files changed, 155 insertions(+) diff --git a/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java index 6959a7f8177..9d7f3c5a92e 100644 --- a/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java +++ b/plugins/src/main/java/common/org/tron/plugins/KeystoreCliUtils.java @@ -15,7 +15,11 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; +import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -81,6 +85,20 @@ static byte[] readRegularFile(File file, long maxSize, String label, PrintWriter return null; } + // Refuse secret files readable or writable by group/other. Only owner + // bits are allowed; filesystems without POSIX attributes are skipped. + PosixFileAttributeView posixView = Files.getFileAttributeView( + file.toPath(), PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + if (posixView != null + && !EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE) + .containsAll(posixView.readAttributes().permissions())) { + err.println(label + " must be owner-only (no group/other access), got " + + PosixFilePermissions.toString(posixView.readAttributes().permissions()) + + ": " + file.getPath()); + return null; + } + int size = (int) attrs.size(); java.util.Set openOptions = new HashSet<>(); openOptions.add(StandardOpenOption.READ); diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java index 29782af91e2..ccbd1092b10 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; @@ -165,6 +166,7 @@ public void testCheckFileExistsPresent() throws Exception { public void testReadPasswordFromFile() throws Exception { File pwFile = tempFolder.newFile("pw.txt"); Files.write(pwFile.toPath(), "goodpassword".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); assertEquals("goodpassword", pw); @@ -174,6 +176,7 @@ public void testReadPasswordFromFile() throws Exception { public void testReadPasswordFromFileWithLineEndings() throws Exception { File pwFile = tempFolder.newFile("pw-crlf.txt"); Files.write(pwFile.toPath(), "goodpassword\r\n".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); assertEquals("goodpassword", pw); @@ -184,6 +187,7 @@ public void testReadPasswordFromFileWithBom() throws Exception { File pwFile = tempFolder.newFile("pw-bom.txt"); Files.write(pwFile.toPath(), "\uFEFFgoodpassword".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); assertEquals("goodpassword", pw); @@ -205,6 +209,7 @@ public void testReadPasswordFileTooLarge() throws Exception { public void testReadPasswordFileShort() throws Exception { File pwFile = tempFolder.newFile("pw-short.txt"); Files.write(pwFile.toPath(), "abc".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); String pw = KeystoreCliUtils.readPassword(pwFile, new PrintWriter(err)); assertNull(pw); @@ -270,6 +275,7 @@ public void testPrintSecurityTipsIncludesAddressAndFile() { public void testReadRegularFileSuccess() throws Exception { File f = tempFolder.newFile("regular.txt"); Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(f); StringWriter err = new StringWriter(); byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", @@ -339,6 +345,7 @@ public void testReadRegularFileRefusesDirectory() throws Exception { @Test public void testReadRegularFileEmptyFile() throws Exception { File f = tempFolder.newFile("empty.txt"); + makeOwnerOnly(f); StringWriter err = new StringWriter(); byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, "File", @@ -346,4 +353,39 @@ public void testReadRegularFileEmptyFile() throws Exception { assertNotNull(bytes); assertEquals(0, bytes.length); } + + private static void makeOwnerOnly(File f) throws IOException { + org.junit.Assume.assumeTrue("POSIX permissions required", + Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) != null); + Files.setPosixFilePermissions(f.toPath(), + java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); + } + + @Test + public void testReadRegularFileRejectsGroupReadable() throws Exception { + File f = tempFolder.newFile("group-readable.txt"); + Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(f); + Files.setPosixFilePermissions(f.toPath(), + java.nio.file.attribute.PosixFilePermissions.fromString("rw-r-----")); + StringWriter err = new StringWriter(); + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, + "Password file", new PrintWriter(err)); + assertNull(bytes); + assertTrue("expected owner-only error, got: " + err, + err.toString().contains("owner-only")); + } + + @Test + public void testReadRegularFileAcceptsOwnerOnly() throws Exception { + File f = tempFolder.newFile("owner-only.txt"); + Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(f); + StringWriter err = new StringWriter(); + byte[] bytes = KeystoreCliUtils.readRegularFile(f, 1024, + "Password file", new PrintWriter(err)); + assertNotNull(bytes); + assertEquals("hello", new String(bytes, StandardCharsets.UTF_8)); + } } diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java index 577889fe196..5034e59a7da 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.SecureRandom; @@ -37,9 +38,11 @@ public void testImportWithKeyFileAndPasswordFile() throws Exception { File keyFile = tempFolder.newFile("private.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("password.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -65,9 +68,11 @@ 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)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -85,9 +90,11 @@ public void testImportInvalidKeyNonHex() throws Exception { Files.write(keyFile.toPath(), "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" .getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -103,6 +110,7 @@ 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)); + makeOwnerOnly(pwFile); // No --key-file and System.console() is null in CI CommandLine cmd = new CommandLine(new Toolkit()); @@ -123,8 +131,10 @@ public void testImportWithSm2() throws Exception { File keyFile = tempFolder.newFile("sm2.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-sm2.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -155,8 +165,10 @@ public void testImportKeyFileWithWhitespace() throws Exception { File keyFile = tempFolder.newFile("ws.key"); Files.write(keyFile.toPath(), (" " + privateKeyHex + " \n\n").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-ws.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -183,8 +195,10 @@ public void testImportDuplicateAddressBlocked() throws Exception { File keyFile = tempFolder.newFile("dup.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-dup.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); // First import succeeds CommandLine cmd1 = new CommandLine(new Toolkit()); @@ -219,8 +233,10 @@ public void testImportDuplicateAddressWithForce() throws Exception { File keyFile = tempFolder.newFile("force.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-force.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); // First import CommandLine cmd1 = new CommandLine(new Toolkit()); @@ -247,6 +263,7 @@ 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)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -268,8 +285,10 @@ public void testImportWith0xPrefix() throws Exception { File keyFile = tempFolder.newFile("0x.key"); Files.write(keyFile.toPath(), ("0x" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-0x.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -295,8 +314,10 @@ public void testImportWith0XUppercasePrefix() throws Exception { File keyFile = tempFolder.newFile("0X.key"); Files.write(keyFile.toPath(), ("0X" + privateKeyHex).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-0X.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -320,8 +341,10 @@ public void testImportWarnsOnCorruptedFile() throws Exception { File keyFile = tempFolder.newFile("warn.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-warn.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); java.io.StringWriter out = new java.io.StringWriter(); java.io.StringWriter err = new java.io.StringWriter(); @@ -352,8 +375,10 @@ public void testImportKeystoreFilePermissions() throws Exception { File keyFile = tempFolder.newFile("perm.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-perm.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -393,6 +418,7 @@ public void testImportRefusesSymlinkKeyFile() throws Exception { File pwFile = tempFolder.newFile("pw-symlink.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); java.io.StringWriter err = new java.io.StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -418,9 +444,11 @@ public void testImportRefusesSymlinkPasswordFile() throws Exception { File keyFile = tempFolder.newFile("sym-pw.key"); Files.write(keyFile.toPath(), ByteArray.toHexString(keyPair.getPrivateKey()).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File realPwFile = tempFolder.newFile("real-pw.txt"); Files.write(realPwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(realPwFile); File pwSymlink = new File(tempFolder.getRoot(), "pw-symlink.txt"); Files.createSymbolicLink(pwSymlink.toPath(), realPwFile.toPath()); @@ -453,8 +481,10 @@ public void testImportDuplicateCheckSkipsInvalidVersion() throws Exception { File keyFile = tempFolder.newFile("ver.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-ver.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "import", @@ -486,8 +516,10 @@ public void testImportDuplicateScanSkipsSymlinkedEntry() throws Exception { File keyFile = tempFolder.newFile("dup-sym.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("pw-dup-sym.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); java.io.StringWriter err = new java.io.StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -515,9 +547,11 @@ public void testImportRejectsMultiLinePasswordFile() throws Exception { File keyFile = tempFolder.newFile("multi.key"); Files.write(keyFile.toPath(), privateKeyHex.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(keyFile); File pwFile = tempFolder.newFile("multi-pw.txt"); Files.write(pwFile.toPath(), "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); java.io.StringWriter err = new java.io.StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -534,4 +568,12 @@ public void testImportRejectsMultiLinePasswordFile() throws Exception { assertTrue("No keystore should have been created", files == null || files.length == 0); } + + private static void makeOwnerOnly(File f) throws IOException { + org.junit.Assume.assumeTrue("POSIX permissions required", + Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) != null); + Files.setPosixFilePermissions(f.toPath(), + java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); + } } diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java index 26e3a9b9764..dbcdf8c2eb7 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; @@ -27,6 +28,7 @@ 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)); + makeOwnerOnly(pwFile); StringWriter out = new StringWriter(); StringWriter err = new StringWriter(); @@ -55,6 +57,7 @@ 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)); + makeOwnerOnly(pwFile); StringWriter out = new StringWriter(); StringWriter err = new StringWriter(); @@ -79,6 +82,7 @@ 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)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -97,6 +101,7 @@ 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)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -128,6 +133,7 @@ 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)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -146,6 +152,7 @@ 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)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -170,6 +177,7 @@ public void testNewKeystoreSpecialCharPassword() throws Exception { File pwFile = tempFolder.newFile("pw-special.txt"); String password = "p@$$w0rd!#%^&*()_+-=[]{}"; Files.write(pwFile.toPath(), password.getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -203,6 +211,7 @@ 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)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -219,6 +228,7 @@ public void testNewKeystorePasswordFileTooLarge() throws Exception { byte[] bigContent = new byte[1025]; java.util.Arrays.fill(bigContent, (byte) 'a'); Files.write(pwFile.toPath(), bigContent); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -238,6 +248,7 @@ public void testNewKeystorePasswordFileWithBom() throws Exception { File pwFile = tempFolder.newFile("bom.txt"); Files.write(pwFile.toPath(), ("\uFEFF" + "test123456").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -259,6 +270,7 @@ public void testNewKeystoreFilePermissions() throws Exception { File dir = tempFolder.newFolder("keystore-perms"); File pwFile = tempFolder.newFile("pw-perms.txt"); Files.write(pwFile.toPath(), "test123456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "new", @@ -289,6 +301,7 @@ public void testNewKeystoreRejectsMultiLinePasswordFile() throws Exception { File pwFile = tempFolder.newFile("multi-line.txt"); Files.write(pwFile.toPath(), "oldpass123\nnewpass456".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -305,4 +318,12 @@ public void testNewKeystoreRejectsMultiLinePasswordFile() throws Exception { assertTrue("No keystore should have been created", files == null || files.length == 0); } + + private static void makeOwnerOnly(File f) throws IOException { + org.junit.Assume.assumeTrue("POSIX permissions required", + Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) != null); + Files.setPosixFilePermissions(f.toPath(), + java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); + } } diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java index ed8f81acd32..1ca4d595aad 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; @@ -48,6 +49,7 @@ public void testUpdatePassword() throws Exception { File pwFile = tempFolder.newFile("passwords.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", address, @@ -84,6 +86,7 @@ public void testUpdateWrongOldPassword() throws Exception { File pwFile = tempFolder.newFile("wrong.txt"); Files.write(pwFile.toPath(), ("wrongpass1\nnewpass456").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -114,6 +117,7 @@ public void testUpdateNonExistentAddress() throws Exception { File pwFile = tempFolder.newFile("pw.txt"); Files.write(pwFile.toPath(), ("test123456\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -142,6 +146,7 @@ public void testUpdateNewPasswordTooShort() throws Exception { File pwFile = tempFolder.newFile("shortpw.txt"); Files.write(pwFile.toPath(), (password + "\nabc").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -171,6 +176,7 @@ public void testUpdateWithWindowsLineEndings() throws Exception { File pwFile = tempFolder.newFile("crlf.txt"); Files.write(pwFile.toPath(), (oldPassword + "\r\n" + newPassword + "\r\n").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", creds.getAddress(), @@ -200,6 +206,7 @@ public void testUpdateJsonOutput() throws Exception { File pwFile = tempFolder.newFile("pw-json.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter out = new StringWriter(); StringWriter err = new StringWriter(); @@ -238,6 +245,7 @@ public void testUpdateWarnsOnCorruptedFile() throws Exception { File pwFile = tempFolder.newFile("pw-corrupt.txt"); Files.write(pwFile.toPath(), (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter out = new StringWriter(); StringWriter err = new StringWriter(); @@ -267,6 +275,7 @@ public void testUpdatePasswordFileOnlyOneLine() throws Exception { File pwFile = tempFolder.newFile("oneline.txt"); Files.write(pwFile.toPath(), "onlyoldpassword".getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -302,6 +311,7 @@ public void testUpdatePasswordFileThreeLines() throws Exception { File pwFile = tempFolder.newFile("threeline.txt"); Files.write(pwFile.toPath(), (oldPassword + "\nnewpass456\nnewpass456").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -387,6 +397,7 @@ public void testUpdateSm2Keystore() throws Exception { File pwFile = tempFolder.newFile("pw-sm2.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", creds.getAddress(), @@ -420,6 +431,7 @@ public void testUpdateMultipleKeystoresSameAddress() throws Exception { File pwFile = tempFolder.newFile("pw-multi.txt"); Files.write(pwFile.toPath(), (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -451,6 +463,7 @@ public void testUpdatePasswordFileTooLarge() throws Exception { byte[] bigContent = new byte[1025]; java.util.Arrays.fill(bigContent, (byte) 'a'); Files.write(pwFile.toPath(), bigContent); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -481,6 +494,7 @@ public void testUpdatePasswordFileWithBom() throws Exception { File pwFile = tempFolder.newFile("bom.txt"); Files.write(pwFile.toPath(), ("\uFEFF" + oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", creds.getAddress(), @@ -502,6 +516,7 @@ public void testUpdateNonExistentKeystoreDir() throws Exception { File pwFile = tempFolder.newFile("pw-nodir.txt"); Files.write(pwFile.toPath(), ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -522,6 +537,7 @@ public void testUpdateKeystoreDirIsFile() throws Exception { File pwFile = tempFolder.newFile("pw-notdir.txt"); Files.write(pwFile.toPath(), ("oldpass123\nnewpass456").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -552,6 +568,7 @@ public void testUpdateWithOldMacLineEndings() throws Exception { File pwFile = tempFolder.newFile("cr.txt"); Files.write(pwFile.toPath(), (oldPassword + "\r" + newPassword + "\r").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", creds.getAddress(), @@ -584,6 +601,7 @@ public void testUpdateSkipsInvalidVersionKeystores() throws Exception { File pwFile = tempFolder.newFile("pw-badver.txt"); Files.write(pwFile.toPath(), (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -624,6 +642,7 @@ public void testUpdateRejectsTamperedAddressKeystore() throws Exception { File pwFile = tempFolder.newFile("pw-tampered.txt"); Files.write(pwFile.toPath(), (password + "\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -654,6 +673,7 @@ public void testUpdatePreservesCorrectDerivedAddress() throws Exception { File pwFile = tempFolder.newFile("pw-derived.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", originalAddress, @@ -702,6 +722,7 @@ public void testUpdateNarrowsLoosePermissionsTo0600() throws Exception { File pwFile = tempFolder.newFile("pw-perms.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); CommandLine cmd = new CommandLine(new Toolkit()); int exitCode = cmd.execute("keystore", "update", creds.getAddress(), @@ -738,6 +759,7 @@ public void testUpdateLegacyTipFiresWhenPasswordHasWhitespace() throws Exception File pwFile = tempFolder.newFile("pw-ws.txt"); Files.write(pwFile.toPath(), ("correct horse battery staple\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -769,6 +791,7 @@ public void testUpdateLegacyTipSuppressedWhenPasswordHasNoWhitespace() throws Ex File pwFile = tempFolder.newFile("pw-nows.txt"); Files.write(pwFile.toPath(), ("wrongpassword\nnewpass789").getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -808,6 +831,7 @@ public void testUpdateScanSkipsSymlinkedEntry() throws Exception { File pwFile = tempFolder.newFile("pw-update-sym.txt"); Files.write(pwFile.toPath(), (oldPassword + "\n" + newPassword).getBytes(StandardCharsets.UTF_8)); + makeOwnerOnly(pwFile); StringWriter err = new StringWriter(); CommandLine cmd = new CommandLine(new Toolkit()); @@ -820,4 +844,12 @@ public void testUpdateScanSkipsSymlinkedEntry() throws Exception { assertTrue("Scan must warn about the symlinked entry, got: " + err.toString(), err.toString().contains("Warning: skipping symbolic link: evil.json")); } + + private static void makeOwnerOnly(File f) throws IOException { + org.junit.Assume.assumeTrue("POSIX permissions required", + Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) != null); + Files.setPosixFilePermissions(f.toPath(), + java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); + } } From 9b48faf90d9d48e375fd607475869a410642426d Mon Sep 17 00:00:00 2001 From: warku123 Date: Sun, 20 Sep 2026 16:18:06 +0800 Subject: [PATCH 09/23] fix(review): address bot review findings on docker, rpc config, keystore API and docs - Dockerfile: install 'which' (gradlew and bin/FullNode launcher need it); keep headless JRE when removing openjdk-devel (clean_requirements_on_remove=0) - NodeConfig: reject negative maxConnectionIdleInMillis/maxConnectionAgeInMillis at config validation instead of failing late in the Netty builder - Args: warn on whitespace-only --password as well (isNotEmpty) - Wallet.create: validate scrypt n/p bounds, symmetric with validate()/decrypt() - docs/configuration.md + reference.conf: 0 means secure default (60 s), not 'no limit' - plugins/README.md: document owner-only (chmod 600) requirement for --password-file/--key-file - tests: negative idle/age rejection; create() bounds rejection + roundtrip --- .../org/tron/core/config/args/NodeConfig.java | 8 ++++- common/src/main/resources/reference.conf | 4 +-- .../tron/core/config/args/NodeConfigTest.java | 22 +++++++++++++ .../main/java/org/tron/keystore/Wallet.java | 8 +++++ docker/Dockerfile | 4 +-- docs/configuration.md | 2 +- .../java/org/tron/core/config/args/Args.java | 2 +- .../keystore/WalletDecryptBoundsTest.java | 32 +++++++++++++++++++ plugins/README.md | 2 ++ 9 files changed, 77 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 78b5c5af13e..0fc26a645f7 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -394,10 +394,16 @@ private void postProcess() { + "maxRstStream={}, secondsPerWindow={})", rpc.maxRstStream, rpc.secondsPerWindow); } + if (rpc.maxConnectionIdleInMillis < 0 || rpc.maxConnectionAgeInMillis < 0) { + throw new TronError("node.rpc.maxConnectionIdleInMillis and node.rpc.maxConnectionAgeInMillis " + + "must be non-negative, got: maxConnectionIdleInMillis=" + rpc.maxConnectionIdleInMillis + + ", maxConnectionAgeInMillis=" + rpc.maxConnectionAgeInMillis, PARAMETER_INIT); + } // 0 (the default) means "use the secure built-in default" (60s), NOT // unlimited: the old 0 -> Long.MAX_VALUE conversion silently turned an // unset or explicitly-zero value into an unbounded connection lifetime. - // Operators who want a longer lifetime must set an explicit positive value. + // Negative values are rejected outright. Operators who want a longer + // lifetime must set an explicit positive value. if (rpc.maxConnectionIdleInMillis == 0) { rpc.maxConnectionIdleInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index d8c483d932a..019f1d22e86 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -295,10 +295,10 @@ node { # HTTP/2 flow control window (bytes), default 1MB flowControlWindow = 1048576 - # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = no limit + # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = secure default (60 s) maxConnectionIdleInMillis = 0 - # Connection max age (ms). 0 = no limit + # Connection max age (ms). 0 = secure default (60 s) maxConnectionAgeInMillis = 0 # Maximum gRPC message size in bytes (default 4194304, ~4MB). diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 8400662b8e6..3e838feb43f 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -148,6 +148,28 @@ public void testRpcNegativeConcurrentCallsRejected() { "node.rpc.maxConcurrentCallsPerConnection must be non-negative, got: -1")); } + @Test + public void testRpcNegativeIdleAndAgeRejected() { + Config config = withRef( + "node { rpc { maxConnectionIdleInMillis = -1, maxConnectionAgeInMillis = 60000 } }"); + + TronError exception = assertThrows(TronError.class, + () -> NodeConfig.fromConfig(config)); + + assertTrue(exception.getMessage().contains( + "node.rpc.maxConnectionIdleInMillis and node.rpc.maxConnectionAgeInMillis " + + "must be non-negative, got: maxConnectionIdleInMillis=-1")); + + Config config2 = withRef( + "node { rpc { maxConnectionIdleInMillis = 60000, maxConnectionAgeInMillis = -5 } }"); + + exception = assertThrows(TronError.class, + () -> NodeConfig.fromConfig(config2)); + + assertTrue(exception.getMessage().contains( + "maxConnectionAgeInMillis=-5")); + } + @Test public void testRpcExplicitZeroIdleAndAgeFallsBackToSecureDefault() { // Explicit 0 no longer means unlimited: postProcess converts 0 to the diff --git a/crypto/src/main/java/org/tron/keystore/Wallet.java b/crypto/src/main/java/org/tron/keystore/Wallet.java index 7218e817e4d..7c05673f77b 100644 --- a/crypto/src/main/java/org/tron/keystore/Wallet.java +++ b/crypto/src/main/java/org/tron/keystore/Wallet.java @@ -81,6 +81,14 @@ public class Wallet { public static WalletFile create(String password, SignInterface sign, int n, int p) throws CipherException { + // Keep create() symmetric with validate()/decrypt(): reject KDF cost + // parameters that the decryption path would refuse, so create() can + // never produce a wallet file that only this class rejects. + String paramError = scryptKdfParamsError(DKLEN, n, R, p); + if (paramError != null) { + throw new CipherException(paramError); + } + byte[] salt = generateRandomBytes(32); byte[] derivedKey = generateDerivedScryptKey(password.getBytes(UTF_8), salt, n, R, p, DKLEN); diff --git a/docker/Dockerfile b/docker/Dockerfile index 97265016810..0aa023bda1c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ ENV TMP_DIR="/tron-build" ENV BASE_DIR="/java-tron" RUN set -o errexit -o nounset \ - && dnf -y install git wget unzip java-1.8.0-openjdk-devel \ + && dnf -y install git wget unzip which java-1.8.0-openjdk-devel \ && echo "git clone" \ && mkdir -p $TMP_DIR \ && cd $TMP_DIR \ @@ -17,7 +17,7 @@ RUN set -o errexit -o nounset \ && mv java-tron-1.0.0 $BASE_DIR \ && rm -rf $TMP_DIR \ && rm -rf ~/.gradle \ - && dnf -y remove java-1.8.0-openjdk-devel \ + && dnf -y --setopt=clean_requirements_on_remove=0 remove java-1.8.0-openjdk-devel \ && dnf clean all RUN wget -P $BASE_DIR/config https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/main_net_config.conf diff --git a/docs/configuration.md b/docs/configuration.md index 6d28e6e63d6..ee613f3fcdd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -106,7 +106,7 @@ node { solidityPort = 50061 # Maximum concurrent calls per connection. 0 uses the secure default of 100. maxConcurrentCallsPerConnection = 100 - # Idle connection timeout (ms). 0 = no limit. + # Idle connection timeout (ms). 0 = secure default (60 s). maxConnectionIdleInMillis = 0 # Minimum active connections required before broadcasting transactions. minEffectiveConnection = 1 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 ef741ef504f..793cdb0a2d2 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 @@ -926,7 +926,7 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { // path 3: config localwitnesskeystore + password if (!lwConfig.getKeystores().isEmpty()) { - if (StringUtils.isNotBlank(cmd.password)) { + if (StringUtils.isNotEmpty(cmd.password)) { logger.warn("Passing the keystore password via --password is insecure: it is visible " + "in the process list and shell history. Omit --password to enter it " + "interactively when prompted."); diff --git a/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java index 353d7732b05..e799e54fa3d 100644 --- a/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java +++ b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java @@ -1,5 +1,6 @@ package org.tron.keystore; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -8,6 +9,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; 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; /** @@ -255,4 +259,32 @@ public void testDecryptCorruptedKeystoreThrowsCipherException() throws Exception () -> Wallet.decrypt("password123", walletFile, true)); assertEquals("Invalid password provided", err.getMessage()); } + + // ---------- create(): KDF params validated symmetric with decrypt() ---------- + + @Test + public void testCreateRejectsOutOfBoundsKdfParams() { + // p=16 exceeds the SCRYPT_P_MAX=8 bound that validate()/decrypt() enforce, + // so create(n=2^18, p=16) would previously produce a wallet file that only + // this class would later refuse to decrypt. Must throw before any KDF run. + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + + CipherException err = assertThrows(CipherException.class, + () -> Wallet.create("password123", keyPair, 1 << 18, 16)); + assertTrue(err.getMessage(), err.getMessage().contains("Scrypt p")); + } + + @Test(timeout = 60000) + public void testCreateAcceptsInBoundsKdfParams() throws Exception { + // n=2^12 (4096), p=1 is well within the enforced bounds; create must + // succeed and the resulting file must pass validate() and decrypt(). + SignInterface keyPair = SignUtils.getGeneratedRandomSign(Utils.getRandom(), true); + byte[] originalKey = keyPair.getPrivateKey(); + + WalletFile walletFile = Wallet.create("password123", keyPair, 1 << 12, 1); + Wallet.validate(walletFile); + + SignInterface recovered = Wallet.decrypt("password123", walletFile, true); + assertArrayEquals(originalKey, recovered.getPrivateKey()); + } } diff --git a/plugins/README.md b/plugins/README.md index f14e070c01a..857ee229c3f 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -222,3 +222,5 @@ When using `--password-file` with `update`, the file must contain exactly two li - `--sm2`: Use SM2 algorithm instead of ECDSA (for `new` and `import`). - `--json`: Output in JSON format for scripting. - `-h | --help`: Provide the help info. + +> **Note:** `--password-file` and `--key-file` must be owner-only (`chmod 600`). Files readable or writable by group/other are rejected — create them with a restrictive umask or run `chmod 600 ` before use. From 7c67b40f32563b088c91de16bd19f418be995e1b Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 21 Sep 2026 11:08:23 +0800 Subject: [PATCH 10/23] fix(build): restore ProtocGenVersion 1.60 pin for centos7 compatibility --- build.gradle | 3 ++- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6dd3bd95ab6..00d7db1405a 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,8 @@ ext.archInfo = [ RocksdbVersion: isArm64 ? '9.7.4' : '5.15.10', // https://github.com/grpc/grpc-java/issues/7690 // https://github.com/grpc/grpc-java/pull/12319, Add support for macOS aarch64 with universal binary - ProtocGenVersion: rootProject.grpcVersion + // https://github.com/grpc/grpc-java/pull/11371 , 1.64.x is not supported CentOS 7. + ProtocGenVersion: isArm64 || isMac ? rootProject.grpcVersion : '1.60.0' ], VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions" ] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d8bbb8f121a..163b793c105 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1238,6 +1238,14 @@ + + + + + + + + From e9638591b19f800a44b70f15cbc4a680c30d8064 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 21 Sep 2026 11:40:58 +0800 Subject: [PATCH 11/23] revert(docker): drop docker-side changes from this PR Per review feedback, docker image/entrypoint changes will be handled in a dedicated ops-side change instead of this audit-fix PR. --- docker/Dockerfile | 25 ++++++++++++++++++++----- docker/docker-entrypoint.sh | 1 + 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0aa023bda1c..2732f5a55ed 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,21 @@ -FROM rockylinux:8 +FROM tronprotocol/centos7:0.2 ENV TMP_DIR="/tron-build" +ENV JDK_TAR="jdk-8u202-linux-x64.tar.gz" +ENV JDK_DIR="jdk1.8.0_202" +ENV JDK_MD5="0029351f7a946f6c05b582100c7d45b7" ENV BASE_DIR="/java-tron" + RUN set -o errexit -o nounset \ - && dnf -y install git wget unzip which java-1.8.0-openjdk-devel \ + && yum -y install git wget \ + && wget -P /usr/local https://github.com/frekele/oracle-java/releases/download/8u202-b08/$JDK_TAR \ + && echo "$JDK_MD5 /usr/local/$JDK_TAR" | md5sum -c \ + && tar -zxf /usr/local/$JDK_TAR -C /usr/local\ + && rm /usr/local/$JDK_TAR \ + && export JAVA_HOME=/usr/local/$JDK_DIR \ + && export CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar \ + && export PATH=$PATH:$JAVA_HOME/bin \ && echo "git clone" \ && mkdir -p $TMP_DIR \ && cd $TMP_DIR \ @@ -13,15 +24,19 @@ RUN set -o errexit -o nounset \ && git checkout master \ && ./gradlew build -x test \ && cd build/distributions \ - && unzip -q java-tron-1.0.0.zip \ + && 7za x -y java-tron-1.0.0.zip \ && mv java-tron-1.0.0 $BASE_DIR \ && rm -rf $TMP_DIR \ && rm -rf ~/.gradle \ - && dnf -y --setopt=clean_requirements_on_remove=0 remove java-1.8.0-openjdk-devel \ - && dnf clean all + && mv $JAVA_HOME/jre /usr/local \ + && rm -rf $JAVA_HOME \ + && yum clean all RUN wget -P $BASE_DIR/config https://raw.githubusercontent.com/tronprotocol/tron-deployment/master/main_net_config.conf +ENV JAVA_HOME="/usr/local/jre" +ENV PATH=$PATH:$JAVA_HOME/bin + COPY docker-entrypoint.sh $BASE_DIR/bin WORKDIR $BASE_DIR diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 5df57d1dd45..d3c5d4c65c8 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -2,4 +2,5 @@ set -eo pipefail shopt -s nullglob +echo "./bin/FullNode $@" > command.txt exec "./bin/FullNode" "$@" \ No newline at end of file From fbe5bf9bf34625e3a958288c7a50897f65ec4ea6 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 21 Sep 2026 11:49:22 +0800 Subject: [PATCH 12/23] fix(cli): drop @Deprecated from --password pending a real alternative Per review feedback: --password has no replacement yet (interactive input cannot run headless), so deprecating it is premature. Keep the startup WARN; re-deprecate once --password-file or equivalent lands. --- .../src/main/java/org/tron/core/config/args/CLIParameter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java index f3ffda69770..6bc18f5996b 100644 --- a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java +++ b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java @@ -45,7 +45,6 @@ public class CLIParameter { @Parameter(names = {"--witness-address"}, description = "witness-address") public String witnessAddress; - @Deprecated @Parameter(names = {"--password"}, description = "password") public String password; From 52971f5c07f6a013986e2441f3d8b7d604f37472 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 21 Sep 2026 11:54:04 +0800 Subject: [PATCH 13/23] fix(cli): drop --password startup WARN per review Reviewer prefers no nagging until a real alternative exists; the WARN returns together with the deprecation once --password-file lands. --- framework/src/main/java/org/tron/core/config/args/Args.java | 5 ----- 1 file changed, 5 deletions(-) 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 793cdb0a2d2..e093d00f2bf 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 @@ -926,11 +926,6 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { // path 3: config localwitnesskeystore + password if (!lwConfig.getKeystores().isEmpty()) { - if (StringUtils.isNotEmpty(cmd.password)) { - logger.warn("Passing the keystore password via --password is insecure: it is visible " - + "in the process list and shell history. Omit --password to enter it " - + "interactively when prompted."); - } localWitnesses = WitnessInitializer.initFromKeystore( lwConfig.getKeystores(), cmd.password, lwConfig.getAccountAddress()); return; From 03c88f1ed5384dac0e7e1299415ad9c4d53fa867 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 23 Sep 2026 11:42:44 +0800 Subject: [PATCH 14/23] fix(build): restore trusted-artifacts in verification metadata per review Restore the blanket trust rules (javadoc/sources jars and *-SNAPSHOT) removed in BUILD-01: they are load-bearing for IDE source/javadoc attachment and internal SNAPSHOT workflows even though CI does not exercise them. --- gradle/verification-metadata.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cf40f1b39e7..9d514eb52f5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4,6 +4,11 @@ true false + + + + + From 6b4f194a7df570fc38950be25f22d2f13820bbec Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 23 Sep 2026 11:57:26 +0800 Subject: [PATCH 15/23] fix(build): place trusted-artifacts inside per verification schema --- gradle/verification-metadata.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9d514eb52f5..2e30496116f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3,12 +3,12 @@ true false + + + + + - - - - - @@ -1257,7 +1257,6 @@ - From 094834c692b91f8cb15f5aa3a9cb3086758a3ec9 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 23 Sep 2026 12:25:01 +0800 Subject: [PATCH 16/23] fix(build): restore upstream build.gradle and wrapper properties per review --- build.gradle | 4 +++- gradle/wrapper/gradle-wrapper.properties | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2167a409604..04dee79fbae 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,7 @@ subprojects { buildscript { repositories { mavenCentral() + maven { url 'https://jitpack.io' } maven { url "https://plugins.gradle.org/m2/" } @@ -83,9 +84,10 @@ subprojects { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/plugins-release' } - mavenLocal() + maven { url 'https://jitpack.io' } } dependencies { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2ccb4862b3..3994438e229 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip -distributionSha256Sum=bed1da33cca0f557ab13691c77f38bb67388119e4794d113e051039b80af9bb1 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c30640555ad1a0a65312803600a70f3502435369 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 23 Sep 2026 13:06:51 +0800 Subject: [PATCH 17/23] test(toolkit): run platform-independent keystore tests on non-POSIX systems --- .../java/org/tron/plugins/KeystoreCliUtilsTest.java | 10 +++++++--- .../test/java/org/tron/plugins/KeystoreImportTest.java | 7 ++++--- .../test/java/org/tron/plugins/KeystoreNewTest.java | 7 ++++--- .../test/java/org/tron/plugins/KeystoreUpdateTest.java | 7 ++++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java index ccbd1092b10..6507ec28f86 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreCliUtilsTest.java @@ -355,9 +355,10 @@ public void testReadRegularFileEmptyFile() throws Exception { } private static void makeOwnerOnly(File f) throws IOException { - org.junit.Assume.assumeTrue("POSIX permissions required", - Files.getFileAttributeView(f.toPath(), - java.nio.file.attribute.PosixFileAttributeView.class) != null); + if (Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) == null) { + return; // non-POSIX FS: skip chmod, mirroring KeystoreCliUtils runtime behavior + } Files.setPosixFilePermissions(f.toPath(), java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); } @@ -366,6 +367,9 @@ private static void makeOwnerOnly(File f) throws IOException { public void testReadRegularFileRejectsGroupReadable() throws Exception { File f = tempFolder.newFile("group-readable.txt"); Files.write(f.toPath(), "hello".getBytes(StandardCharsets.UTF_8)); + org.junit.Assume.assumeTrue("POSIX permissions test, skip on Windows", + Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) != null); makeOwnerOnly(f); Files.setPosixFilePermissions(f.toPath(), java.nio.file.attribute.PosixFilePermissions.fromString("rw-r-----")); diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java index 5034e59a7da..b73ecc14d2e 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreImportTest.java @@ -570,9 +570,10 @@ public void testImportRejectsMultiLinePasswordFile() throws Exception { } private static void makeOwnerOnly(File f) throws IOException { - org.junit.Assume.assumeTrue("POSIX permissions required", - Files.getFileAttributeView(f.toPath(), - java.nio.file.attribute.PosixFileAttributeView.class) != null); + if (Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) == null) { + return; // non-POSIX FS: skip chmod, mirroring KeystoreCliUtils runtime behavior + } Files.setPosixFilePermissions(f.toPath(), java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); } diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java index dbcdf8c2eb7..c7192f89111 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreNewTest.java @@ -320,9 +320,10 @@ public void testNewKeystoreRejectsMultiLinePasswordFile() throws Exception { } private static void makeOwnerOnly(File f) throws IOException { - org.junit.Assume.assumeTrue("POSIX permissions required", - Files.getFileAttributeView(f.toPath(), - java.nio.file.attribute.PosixFileAttributeView.class) != null); + if (Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) == null) { + return; // non-POSIX FS: skip chmod, mirroring KeystoreCliUtils runtime behavior + } Files.setPosixFilePermissions(f.toPath(), java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); } diff --git a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java index 1ca4d595aad..066ed070eb6 100644 --- a/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java +++ b/plugins/src/test/java/org/tron/plugins/KeystoreUpdateTest.java @@ -846,9 +846,10 @@ public void testUpdateScanSkipsSymlinkedEntry() throws Exception { } private static void makeOwnerOnly(File f) throws IOException { - org.junit.Assume.assumeTrue("POSIX permissions required", - Files.getFileAttributeView(f.toPath(), - java.nio.file.attribute.PosixFileAttributeView.class) != null); + if (Files.getFileAttributeView(f.toPath(), + java.nio.file.attribute.PosixFileAttributeView.class) == null) { + return; // non-POSIX FS: skip chmod, mirroring KeystoreCliUtils runtime behavior + } Files.setPosixFilePermissions(f.toPath(), java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")); } From f55e3f44e5df8aa3ac61f4e43fc9b9c1bf7e1e0f Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 12 Oct 2026 11:44:28 +0800 Subject: [PATCH 18/23] fix(config): restore unlimited maxConnectionAge default, keep 60s idle default maxConnectionAgeInMillis=0 restores legacy semantics (unlimited) instead of forcing a 60s max age, which would churn active connections on every default node. Idle timeout keeps the 60s secure default; negative values still rejected; positive overrides honored. Per review. --- .../org/tron/core/config/args/NodeConfig.java | 20 +++++++++------- common/src/main/resources/reference.conf | 2 +- .../tron/core/config/args/NodeConfigTest.java | 23 +++++++++++-------- .../org/tron/core/config/args/ArgsTest.java | 6 ++--- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 0fc26a645f7..698932449b9 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -208,8 +208,9 @@ public static class HttpConfig { public static class RpcConfig { public static final int DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION = 100; - // Secure built-in default for gRPC connection lifetime (idle & max age). - // Applied by postProcess() when the configured value is 0 (the default). + // Secure built-in default for the gRPC idle timeout. Applied by + // postProcess() when maxConnectionIdleInMillis is 0 (the default). + // maxConnectionAgeInMillis keeps its legacy meaning: 0 = unlimited. public static final long DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS = 60_000L; private boolean enable = true; @@ -399,16 +400,19 @@ private void postProcess() { + "must be non-negative, got: maxConnectionIdleInMillis=" + rpc.maxConnectionIdleInMillis + ", maxConnectionAgeInMillis=" + rpc.maxConnectionAgeInMillis, PARAMETER_INIT); } - // 0 (the default) means "use the secure built-in default" (60s), NOT - // unlimited: the old 0 -> Long.MAX_VALUE conversion silently turned an - // unset or explicitly-zero value into an unbounded connection lifetime. - // Negative values are rejected outright. Operators who want a longer - // lifetime must set an explicit positive value. + // maxConnectionIdleInMillis: 0 (the default) means "use the secure + // built-in default" (60s), NOT unlimited: the old 0 -> Long.MAX_VALUE + // conversion silently turned an unset or explicitly-zero idle value into + // an unbounded idle timeout. Operators who want a longer idle timeout + // must set an explicit positive value. + // maxConnectionAgeInMillis: keeps the legacy gRPC semantic where + // 0 = unlimited (converted to Long.MAX_VALUE); positive values are + // honored as-is. if (rpc.maxConnectionIdleInMillis == 0) { rpc.maxConnectionIdleInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; } if (rpc.maxConnectionAgeInMillis == 0) { - rpc.maxConnectionAgeInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; + rpc.maxConnectionAgeInMillis = Long.MAX_VALUE; } // validateSignThreadNum: 0 = auto-detect diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 9281c065b60..61700b37f2c 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -298,7 +298,7 @@ node { # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = secure default (60 s) maxConnectionIdleInMillis = 0 - # Connection max age (ms). 0 = secure default (60 s) + # Connection max age (ms). 0 = unlimited maxConnectionAgeInMillis = 0 # Maximum gRPC message size in bytes (default 4194304, ~4MB). diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 3e838feb43f..d0246c86c49 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -107,10 +107,11 @@ public void testRpcDefaultsFromReference() { assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION, rpc.getMaxConcurrentCallsPerConnection()); assertEquals(1048576, rpc.getFlowControlWindow()); - // reference.conf keeps 0 (the default marker); postProcess converts 0 to - // the secure built-in 60s default, so the effective default is 60s. + // reference.conf keeps 0 (the default marker); postProcess converts + // idle 0 to the secure built-in 60s default, while age 0 keeps its + // legacy unlimited meaning. assertEquals(60000L, rpc.getMaxConnectionIdleInMillis()); - assertEquals(60000L, rpc.getMaxConnectionAgeInMillis()); + assertEquals(Long.MAX_VALUE, rpc.getMaxConnectionAgeInMillis()); assertEquals(4194304, rpc.getMaxMessageSize()); assertEquals(8192, rpc.getMaxHeaderListSize()); assertEquals(1, rpc.getMinEffectiveConnection()); @@ -171,16 +172,15 @@ public void testRpcNegativeIdleAndAgeRejected() { } @Test - public void testRpcExplicitZeroIdleAndAgeFallsBackToSecureDefault() { - // Explicit 0 no longer means unlimited: postProcess converts 0 to the - // secure built-in 60s default. Operators wanting a longer lifetime must - // set an explicit positive value. + public void testRpcExplicitZeroIdleAndAgeFallsBackToDefaults() { + // Idle 0 falls back to the secure built-in 60s default; age 0 keeps its + // legacy unlimited meaning (Long.MAX_VALUE). Config config = withRef( "node { rpc { maxConnectionIdleInMillis = 0, maxConnectionAgeInMillis = 0 } }"); NodeConfig nc = NodeConfig.fromConfig(config); assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS, nc.getRpc().getMaxConnectionIdleInMillis()); - assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS, + assertEquals(Long.MAX_VALUE, nc.getRpc().getMaxConnectionAgeInMillis()); } @@ -242,7 +242,9 @@ public void testRpcUserOverrideExplicitValues() { + " maxConcurrentCallsPerConnection = 50," + " flowControlWindow = 2097152," + " maxMessageSize = 8388608," - + " maxHeaderListSize = 16384 } }"); + + " maxHeaderListSize = 16384," + + " maxConnectionIdleInMillis = 3600000," + + " maxConnectionAgeInMillis = 7200000 } }"); NodeConfig nc = NodeConfig.fromConfig(config); NodeConfig.RpcConfig rpc = nc.getRpc(); assertEquals(32, rpc.getThread()); @@ -250,6 +252,9 @@ public void testRpcUserOverrideExplicitValues() { assertEquals(2097152, rpc.getFlowControlWindow()); assertEquals(8388608, rpc.getMaxMessageSize()); assertEquals(16384, rpc.getMaxHeaderListSize()); + // Explicit positive idle/age values must be honored, not overwritten. + assertEquals(3600000L, rpc.getMaxConnectionIdleInMillis()); + assertEquals(7200000L, rpc.getMaxConnectionAgeInMillis()); } // =========================================================================== 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 4001fc36bae..3ab5fe046ca 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 @@ -108,9 +108,9 @@ public void get() { .assertEquals(NettyServerBuilder .DEFAULT_FLOW_CONTROL_WINDOW, parameter.getFlowControlWindow()); Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); - // config-test.conf leaves maxConnectionAgeInMillis unset; postProcess - // converts the 0 default to the secure built-in 60s lifetime default. - Assert.assertEquals(60000L, parameter.getMaxConnectionAgeInMillis()); + // config-test.conf leaves maxConnectionAgeInMillis unset; 0 keeps its + // legacy unlimited meaning (Long.MAX_VALUE). + 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()); From 0bc24b80feae50bab02a601018d8672561614a79 Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 12 Oct 2026 17:20:39 +0800 Subject: [PATCH 19/23] chore: retrigger CI (flaky debian11 test JVM crash in upstream BufferedResponseWrapperTest) From 3a3794b2dbd2862acbf5c6cce0ee9a6fcbb226fd Mon Sep 17 00:00:00 2001 From: warku123 Date: Mon, 12 Oct 2026 17:49:58 +0800 Subject: [PATCH 20/23] refactor(config): drop rst window validation covered by #7015 --- .../org/tron/core/config/args/NodeConfig.java | 18 ------- .../tron/core/config/args/NodeConfigTest.java | 51 ------------------- 2 files changed, 69 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 6850b4954a5..b4a8f30d660 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -382,24 +382,6 @@ private void postProcess() { rpc.maxConcurrentCallsPerConnection = RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION; } - // node.rpc.maxRstStream and node.rpc.secondsPerWindow only take effect - // together (RpcService applies RST limiting only when both are > 0). A - // half-configured pair would silently disable RST_STREAM flood - // protection — warn loudly so the misconfiguration is visible. - if (rpc.maxRstStream < 0 || rpc.secondsPerWindow < 0) { - throw new TronError("node.rpc.maxRstStream and node.rpc.secondsPerWindow " - + "must be non-negative, got: maxRstStream=" + rpc.maxRstStream - + ", secondsPerWindow=" + rpc.secondsPerWindow, PARAMETER_INIT); - } - if ((rpc.maxRstStream > 0 && rpc.secondsPerWindow <= 0) - || (rpc.maxRstStream <= 0 && rpc.secondsPerWindow > 0)) { - logger.warn("node.rpc.maxRstStream and node.rpc.secondsPerWindow only " - + "take effect together: exactly one of them is set while the other " - + "is 0, so RST_STREAM flood protection is DISABLED. Set both to " - + "positive values to enable it, or leave both at 0 (got: " - + "maxRstStream={}, secondsPerWindow={})", - rpc.maxRstStream, rpc.secondsPerWindow); - } if (rpc.maxConnectionIdleInMillis < 0 || rpc.maxConnectionAgeInMillis < 0) { throw new TronError("node.rpc.maxConnectionIdleInMillis and node.rpc.maxConnectionAgeInMillis " + "must be non-negative, got: maxConnectionIdleInMillis=" + rpc.maxConnectionIdleInMillis diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index d0246c86c49..46027acdc1d 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -184,57 +184,6 @@ public void testRpcExplicitZeroIdleAndAgeFallsBackToDefaults() { nc.getRpc().getMaxConnectionAgeInMillis()); } - // ----- maxRstStream / secondsPerWindow: must be configured together ----- - - @Test - public void testRpcRstPairBothZeroOk() { - // Default: feature off. Must not throw. - NodeConfig nc = NodeConfig.fromConfig(withRef()); - assertEquals(0, nc.getRpc().getMaxRstStream()); - assertEquals(0, nc.getRpc().getSecondsPerWindow()); - } - - @Test - public void testRpcRstPairBothPositiveOk() { - Config config = withRef( - "node { rpc { maxRstStream = 1000, secondsPerWindow = 60 } }"); - NodeConfig nc = NodeConfig.fromConfig(config); - assertEquals(1000, nc.getRpc().getMaxRstStream()); - assertEquals(60, nc.getRpc().getSecondsPerWindow()); - } - - private static TronError rstError(String hocon) { - Config config = withRef("node { rpc { " + hocon + " } }"); - return assertThrows(TronError.class, () -> NodeConfig.fromConfig(config)); - } - - @Test - public void testRpcRstPairHalfConfiguredWarnsButStarts() { - // Only maxRstStream set: RST flood protection stays disabled, startup - // continues with a warning. - Config config = withRef("node { rpc { maxRstStream = 100 } }"); - NodeConfig nc = NodeConfig.fromConfig(config); - assertEquals(100, nc.getRpc().getMaxRstStream()); - assertEquals(0, nc.getRpc().getSecondsPerWindow()); - - // Only secondsPerWindow set: same trap, same warn-and-continue. - config = withRef("node { rpc { secondsPerWindow = 60 } }"); - nc = NodeConfig.fromConfig(config); - assertEquals(0, nc.getRpc().getMaxRstStream()); - assertEquals(60, nc.getRpc().getSecondsPerWindow()); - } - - @Test - public void testRpcRstNegativeValuesRejected() { - TronError exception = rstError("maxRstStream = -1, secondsPerWindow = 60"); - assertTrue(exception.getMessage().contains( - "must be non-negative, got: maxRstStream=-1")); - - exception = rstError("maxRstStream = 100, secondsPerWindow = -5"); - assertTrue(exception.getMessage().contains( - "must be non-negative, got: maxRstStream=100, secondsPerWindow=-5")); - } - @Test public void testRpcUserOverrideExplicitValues() { Config config = withRef( From afbe671bfe0bd76e41e22aaef757e74ab3ed8182 Mon Sep 17 00:00:00 2001 From: warku123 Date: Tue, 13 Oct 2026 18:55:47 +0800 Subject: [PATCH 21/23] test(keystore): cover scrypt runtime-exception wrapping in decrypt --- .../tron/keystore/WalletDecryptBoundsTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java index 61f0168b01f..aab7b640b9d 100644 --- a/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java +++ b/framework/src/test/java/org/tron/keystore/WalletDecryptBoundsTest.java @@ -260,6 +260,24 @@ public void testDecryptCorruptedKeystoreThrowsCipherException() throws Exception assertEquals("Invalid password provided", err.getMessage()); } + @Test + public void testDecryptWrapsScryptRuntimeException() throws Exception { + // r=1 with n=131072 passes every validation bound (n is a power of 2 in + // [2^12, 2^20], r=1, p=1, 128*r*n = 16 MiB, dklen=32), but Bouncy + // Castle's SCrypt.generate itself rejects r==1 with n >= 65536 and throws + // a raw IllegalArgumentException. decrypt() must wrap that runtime KDF + // engine failure into a clean CipherException, preserving the cause. + WalletFile walletFile = parseKeystore(String.format(SCRYPT_TEMPLATE, + 32, 1 << 17, 1, 1)); + Wallet.validate(walletFile); + + CipherException err = assertThrows(CipherException.class, + () -> Wallet.decrypt("password123", walletFile)); + assertEquals("Scrypt key derivation failed", err.getMessage()); + assertTrue("cause must be the SCrypt IllegalArgumentException", + err.getCause() instanceof IllegalArgumentException); + } + // ---------- create(): KDF params validated symmetric with decrypt() ---------- @Test From 23d96dcfb26dc9a24261677b38de560977b0d792 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 14 Oct 2026 12:01:36 +0800 Subject: [PATCH 22/23] fix(config): set recommended grpc idle timeout without changing zero semantics --- .../org/tron/core/config/args/NodeConfig.java | 19 +------- common/src/main/resources/reference.conf | 6 +-- .../tron/core/config/args/NodeConfigTest.java | 48 ++++--------------- docs/configuration.md | 4 +- .../org/tron/core/config/args/ArgsTest.java | 2 - 5 files changed, 15 insertions(+), 64 deletions(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 040c9da0950..56d5cd7cca3 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -207,10 +207,6 @@ public static class HttpConfig { public static class RpcConfig { public static final int DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION = 100; - // Secure built-in default for the gRPC idle timeout. Applied by - // postProcess() when maxConnectionIdleInMillis is 0 (the default). - // maxConnectionAgeInMillis keeps its legacy meaning: 0 = unlimited. - public static final long DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS = 60_000L; public static final int DEFAULT_MAX_RST_STREAM = 1000; public static final int DEFAULT_SECONDS_PER_WINDOW = 5; @@ -407,21 +403,8 @@ private void postProcess() { + "protection; using the secure default of {}.", RpcConfig.DEFAULT_SECONDS_PER_WINDOW); rpc.secondsPerWindow = RpcConfig.DEFAULT_SECONDS_PER_WINDOW; } - if (rpc.maxConnectionIdleInMillis < 0 || rpc.maxConnectionAgeInMillis < 0) { - throw new TronError("node.rpc.maxConnectionIdleInMillis and node.rpc.maxConnectionAgeInMillis " - + "must be non-negative, got: maxConnectionIdleInMillis=" + rpc.maxConnectionIdleInMillis - + ", maxConnectionAgeInMillis=" + rpc.maxConnectionAgeInMillis, PARAMETER_INIT); - } - // maxConnectionIdleInMillis: 0 (the default) means "use the secure - // built-in default" (60s), NOT unlimited: the old 0 -> Long.MAX_VALUE - // conversion silently turned an unset or explicitly-zero idle value into - // an unbounded idle timeout. Operators who want a longer idle timeout - // must set an explicit positive value. - // maxConnectionAgeInMillis: keeps the legacy gRPC semantic where - // 0 = unlimited (converted to Long.MAX_VALUE); positive values are - // honored as-is. if (rpc.maxConnectionIdleInMillis == 0) { - rpc.maxConnectionIdleInMillis = RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS; + rpc.maxConnectionIdleInMillis = Long.MAX_VALUE; } if (rpc.maxConnectionAgeInMillis == 0) { rpc.maxConnectionAgeInMillis = Long.MAX_VALUE; diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index b8859b6f7c2..49caf28b3af 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -287,10 +287,10 @@ node { # HTTP/2 flow control window (bytes), default 1MB flowControlWindow = 1048576 - # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = secure default (60 s) - maxConnectionIdleInMillis = 0 + # Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = no limit + maxConnectionIdleInMillis = 60000 - # Connection max age (ms). 0 = unlimited + # Connection max age (ms). 0 = no limit maxConnectionAgeInMillis = 0 # Maximum gRPC message size in bytes (default 4194304, ~4MB). diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 2c60c683935..76589b4a82b 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -108,11 +108,10 @@ public void testRpcDefaultsFromReference() { assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION, rpc.getMaxConcurrentCallsPerConnection()); assertEquals(1048576, rpc.getFlowControlWindow()); - // reference.conf keeps 0 (the default marker); postProcess converts - // idle 0 to the secure built-in 60s default, while age 0 keeps its - // legacy unlimited meaning. + // reference.conf recommends 60000 for idle; age stays 0 in + // reference.conf, which postProcess maps to Long.MAX_VALUE. assertEquals(60000L, rpc.getMaxConnectionIdleInMillis()); - assertEquals(Long.MAX_VALUE, rpc.getMaxConnectionAgeInMillis()); + assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis()); assertEquals(4194304, rpc.getMaxMessageSize()); assertEquals(8192, rpc.getMaxHeaderListSize()); assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_RST_STREAM, rpc.getMaxRstStream()); @@ -153,38 +152,14 @@ public void testRpcNegativeConcurrentCallsRejected() { } @Test - public void testRpcNegativeIdleAndAgeRejected() { - Config config = withRef( - "node { rpc { maxConnectionIdleInMillis = -1, maxConnectionAgeInMillis = 60000 } }"); - - TronError exception = assertThrows(TronError.class, - () -> NodeConfig.fromConfig(config)); - - assertTrue(exception.getMessage().contains( - "node.rpc.maxConnectionIdleInMillis and node.rpc.maxConnectionAgeInMillis " - + "must be non-negative, got: maxConnectionIdleInMillis=-1")); - - Config config2 = withRef( - "node { rpc { maxConnectionIdleInMillis = 60000, maxConnectionAgeInMillis = -5 } }"); - - exception = assertThrows(TronError.class, - () -> NodeConfig.fromConfig(config2)); - - assertTrue(exception.getMessage().contains( - "maxConnectionAgeInMillis=-5")); - } - - @Test - public void testRpcExplicitZeroIdleAndAgeFallsBackToDefaults() { - // Idle 0 falls back to the secure built-in 60s default; age 0 keeps its - // legacy unlimited meaning (Long.MAX_VALUE). + public void testRpcExplicitZeroIdleAndAgeMeansUnlimited() { + // Legacy semantic preserved: an explicit 0 for idle/age means unlimited + // (Long.MAX_VALUE). reference.conf recommends 60000 for idle instead. Config config = withRef( "node { rpc { maxConnectionIdleInMillis = 0, maxConnectionAgeInMillis = 0 } }"); NodeConfig nc = NodeConfig.fromConfig(config); - assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONNECTION_LIFETIME_IN_MILLIS, - nc.getRpc().getMaxConnectionIdleInMillis()); - assertEquals(Long.MAX_VALUE, - nc.getRpc().getMaxConnectionAgeInMillis()); + assertEquals(Long.MAX_VALUE, nc.getRpc().getMaxConnectionIdleInMillis()); + assertEquals(Long.MAX_VALUE, nc.getRpc().getMaxConnectionAgeInMillis()); } @Test @@ -251,9 +226,7 @@ public void testRpcUserOverrideExplicitValues() { + " maxConcurrentCallsPerConnection = 50," + " flowControlWindow = 2097152," + " maxMessageSize = 8388608," - + " maxHeaderListSize = 16384," - + " maxConnectionIdleInMillis = 3600000," - + " maxConnectionAgeInMillis = 7200000 } }"); + + " maxHeaderListSize = 16384 } }"); NodeConfig nc = NodeConfig.fromConfig(config); NodeConfig.RpcConfig rpc = nc.getRpc(); assertEquals(32, rpc.getThread()); @@ -261,9 +234,6 @@ public void testRpcUserOverrideExplicitValues() { assertEquals(2097152, rpc.getFlowControlWindow()); assertEquals(8388608, rpc.getMaxMessageSize()); assertEquals(16384, rpc.getMaxHeaderListSize()); - // Explicit positive idle/age values must be honored, not overwritten. - assertEquals(3600000L, rpc.getMaxConnectionIdleInMillis()); - assertEquals(7200000L, rpc.getMaxConnectionAgeInMillis()); } // =========================================================================== diff --git a/docs/configuration.md b/docs/configuration.md index 0e1363812b2..710311f38e6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,8 +126,8 @@ node { maxRstStream = 1000 # RST_STREAM counting window in seconds. 0 uses the default of 5. secondsPerWindow = 5 - # Idle connection timeout (ms). 0 = secure default (60 s). - maxConnectionIdleInMillis = 0 + # Idle connection timeout (ms). 0 = no limit. + maxConnectionIdleInMillis = 60000 # Minimum active connections required before broadcasting transactions. minEffectiveConnection = 1 } 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 f8874edf048..107625d4893 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 @@ -160,8 +160,6 @@ public void get() { .assertEquals(NettyServerBuilder .DEFAULT_FLOW_CONTROL_WINDOW, parameter.getFlowControlWindow()); Assert.assertEquals(60000L, parameter.getMaxConnectionIdleInMillis()); - // config-test.conf leaves maxConnectionAgeInMillis unset; 0 keeps its - // legacy unlimited meaning (Long.MAX_VALUE). 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()); From cb2d9ca59e516e0dc72748cfd40e1b3f3f1f1bc8 Mon Sep 17 00:00:00 2001 From: warku123 Date: Wed, 14 Oct 2026 14:23:09 +0800 Subject: [PATCH 23/23] fix(config): align grpc idle default with reference config --- common/src/main/java/org/tron/core/config/args/NodeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 56d5cd7cca3..3e60378d86a 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -221,7 +221,7 @@ public static class RpcConfig { private int maxConcurrentCallsPerConnection = DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION; private int flowControlWindow = 1048576; - private long maxConnectionIdleInMillis = 0; + private long maxConnectionIdleInMillis = 60000L; private long maxConnectionAgeInMillis = 0; private int maxMessageSize = 4194304; private int maxHeaderListSize = 8192;