Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6aa4388
fix(keystore): fail fast on password stdin EOF instead of NPE
warku123 Sep 9, 2026
2a4950c
fix(keystore): validate KDF parameter bounds before decrypt
warku123 Sep 9, 2026
6a7000e
feat(cli): deprecate --private-key/--password in favor of localwitnes…
warku123 Sep 9, 2026
d8b8bd1
fix(config): harden gRPC connection lifetime default and warn on half…
warku123 Sep 14, 2026
6ec3d8a
build: pin Gradle distribution, enforce wrapper validation in CI, and…
warku123 Sep 9, 2026
fabe9a0
build(protocol): unify protoc-gen-grpc-java version and rebase docker…
warku123 Sep 9, 2026
f4f946e
fix(docker): stop persisting full node command line to command.txt
warku123 Sep 14, 2026
6129404
fix(toolkit): enforce owner-only permissions on password and key files
warku123 Sep 15, 2026
9b48faf
fix(review): address bot review findings on docker, rpc config, keyst…
warku123 Sep 20, 2026
7c67b40
fix(build): restore ProtocGenVersion 1.60 pin for centos7 compatibility
warku123 Sep 21, 2026
e963859
revert(docker): drop docker-side changes from this PR
warku123 Sep 21, 2026
fbe5bf9
fix(cli): drop @Deprecated from --password pending a real alternative
warku123 Sep 21, 2026
52971f5
fix(cli): drop --password startup WARN per review
warku123 Sep 21, 2026
fa5b0cb
Merge remote-tracking branch 'upstream/release_v4.8.3' into fix/g-ser…
warku123 Sep 21, 2026
03c88f1
fix(build): restore trusted-artifacts in verification metadata per re…
warku123 Sep 23, 2026
6b4f194
fix(build): place trusted-artifacts inside <configuration> per verifi…
warku123 Sep 23, 2026
094834c
fix(build): restore upstream build.gradle and wrapper properties per …
warku123 Sep 23, 2026
c306405
test(toolkit): run platform-independent keystore tests on non-POSIX s…
warku123 Sep 23, 2026
f55e3f4
fix(config): restore unlimited maxConnectionAge default, keep 60s idl…
warku123 Oct 12, 2026
8263813
Merge remote-tracking branch 'upstream/release_v4.8.3' into fix/g-ser…
warku123 Oct 12, 2026
954b777
Merge remote-tracking branch 'upstream/release_v4.8.3' into fix/g-ser…
warku123 Oct 12, 2026
0bc24b8
chore: retrigger CI (flaky debian11 test JVM crash in upstream Buffer…
warku123 Oct 12, 2026
3a3794b
refactor(config): drop rst window validation covered by #7015
warku123 Oct 12, 2026
f2dbe6c
Merge remote-tracking branch 'upstream/release_v4.8.3' into fix/g-ser…
warku123 Oct 13, 2026
afbe671
test(keystore): cover scrypt runtime-exception wrapping in decrypt
warku123 Oct 13, 2026
23d96dc
fix(config): set recommended grpc idle timeout without changing zero …
warku123 Oct 14, 2026
6bdc458
Merge upstream/release_v4.8.3 (a901f85a4a: #7020 http idle timeout at…
warku123 Oct 14, 2026
cb2d9ca
fix(config): align grpc idle default with reference config
warku123 Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/integration-test-single-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ Fill in the private key of your SR account into the `localwitness` list in the c
<your_private_key>
]
```

> **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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion common/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ node {
flowControlWindow = 1048576

# Connection idle timeout (ms). Connections idle longer than this are gracefully terminated. 0 = no limit
maxConnectionIdleInMillis = 0
maxConnectionIdleInMillis = 60000

# Connection max age (ms). 0 = no limit
maxConnectionAgeInMillis = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ public void testRpcDefaultsFromReference() {
assertEquals(NodeConfig.RpcConfig.DEFAULT_MAX_CONCURRENT_CALLS_PER_CONNECTION,
rpc.getMaxConcurrentCallsPerConnection());
assertEquals(1048576, rpc.getFlowControlWindow());
assertEquals(9223372036854775807L, rpc.getMaxConnectionIdleInMillis());
// reference.conf recommends 60000 for idle; age stays 0 in
// reference.conf, which postProcess maps to Long.MAX_VALUE.
assertEquals(60000L, rpc.getMaxConnectionIdleInMillis());
assertEquals(9223372036854775807L, rpc.getMaxConnectionAgeInMillis());
assertEquals(4194304, rpc.getMaxMessageSize());
assertEquals(8192, rpc.getMaxHeaderListSize());
Expand Down Expand Up @@ -149,6 +151,17 @@ public void testRpcNegativeConcurrentCallsRejected() {
"node.rpc.maxConcurrentCallsPerConnection must be non-negative, got: -1"));
}

@Test
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(Long.MAX_VALUE, nc.getRpc().getMaxConnectionIdleInMillis());
assertEquals(Long.MAX_VALUE, nc.getRpc().getMaxConnectionAgeInMillis());
}

@Test
public void testRpcRstDefaultsMatchReference() {
NodeConfig.RpcConfig rpc = new NodeConfig.RpcConfig();
Expand Down
104 changes: 98 additions & 6 deletions crypto/src/main/java/org/tron/keystore/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,32 @@ 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 {

// 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);
Expand Down Expand Up @@ -194,15 +217,28 @@ 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();
int c = aes128CtrKdfParams.getC();
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());
}
Expand Down Expand Up @@ -254,11 +290,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).
*
* <p>Shared by {@link #validate(WalletFile)} (which throws the message)
* and {@link #isValidKeystoreFile(WalletFile)} (which returns boolean
* for discovery-style filtering).
* <p>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) {
Expand All @@ -276,6 +316,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;
}

Expand Down
23 changes: 22 additions & 1 deletion crypto/src/main/java/org/tron/keystore/WalletUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -230,6 +232,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.
Expand All @@ -238,7 +248,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;
Expand Down
7 changes: 4 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ node {
# RST_STREAM counting window in seconds. 0 uses the default of 5.
secondsPerWindow = 5
# Idle connection timeout (ms). 0 = no limit.
maxConnectionIdleInMillis = 0
maxConnectionIdleInMillis = 60000
# Minimum active connections required before broadcasting transactions.
minEffectiveConnection = 1
}
Expand Down Expand Up @@ -180,12 +180,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"
# ]
Expand Down
5 changes: 5 additions & 0 deletions framework/src/main/java/org/tron/core/config/args/Args.java
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,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;
Expand All @@ -909,6 +912,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;
Expand Down
Loading
Loading