Skip to content

Commit bf2c75c

Browse files
halibobo1205claude
andcommitted
fix(consensus): use Locale.ROOT for case-insensitive string operations
String.toLowerCase()/toUpperCase() without an explicit Locale uses Locale.getDefault(), which on Turkish (tr) or Azerbaijani (az) systems folds 'I' to dotless-ı (U+0131) instead of 'i' (U+0069). This caused AccountIdIndexStore to generate different index keys on tr/az nodes, enabling a consensus split where tr/az nodes accept duplicate accountId transactions that other nodes reject. Fix AccountIdIndexStore.getLowerCaseAccountId to use Locale.ROOT, and add a legacy fallback in get()/has() so that tr/az nodes can still read old data stored with the locale-sensitive key. Also fix all other toLowerCase()/toUpperCase() calls across the codebase and enable the built-in ErrorProne StringCaseLocaleUsage checker at ERROR level to prevent future regressions at compile time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1ae9bb3 commit bf2c75c

27 files changed

Lines changed: 618 additions & 36 deletions

File tree

actuator/src/main/java/org/tron/core/actuator/AssetIssueActuator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.ArrayList;
2323
import java.util.Iterator;
2424
import java.util.List;
25+
import java.util.Locale;
2526
import java.util.Objects;
2627
import lombok.extern.slf4j.Slf4j;
2728
import org.tron.common.math.StrictMathWrapper;
@@ -166,7 +167,7 @@ public boolean validate() throws ContractValidateException {
166167
}
167168

168169
if (dynamicStore.getAllowSameTokenName() != 0) {
169-
String name = assetIssueContract.getName().toStringUtf8().toLowerCase();
170+
String name = assetIssueContract.getName().toStringUtf8().toLowerCase(Locale.ROOT);
170171
if (("trx").equals(name)) {
171172
throw new ContractValidateException("assetName can't be trx");
172173
}

build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import org.gradle.nativeplatform.platform.internal.Architectures
22
import org.gradle.internal.os.OperatingSystem
3+
4+
plugins {
5+
id 'net.ltgt.errorprone' version '5.0.0' apply false
6+
}
7+
38
allprojects {
49
version = "1.0.0"
510
apply plugin: "java-library"
611
ext {
712
springVersion = "5.3.39"
13+
errorproneVersion = "2.42.0"
814
}
915
}
1016
def arch = System.getProperty("os.arch").toLowerCase()
@@ -107,6 +113,26 @@ subprojects {
107113
testImplementation "org.mockito:mockito-core:4.11.0"
108114
testImplementation "org.mockito:mockito-inline:4.11.0"
109115
}
116+
if (project.name != 'protocol' && project.name != 'errorprone'
117+
&& javaVersion.isJava11Compatible()) {
118+
apply plugin: 'net.ltgt.errorprone'
119+
dependencies {
120+
errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}"
121+
errorprone rootProject.project(':errorprone')
122+
}
123+
tasks.withType(JavaCompile).configureEach {
124+
options.errorprone {
125+
enabled = true
126+
disableWarningsInGeneratedCode = true
127+
disableAllChecks = true
128+
excludedPaths = '.*/generated/.*'
129+
errorproneArgs.addAll([
130+
'-Xep:StringCaseLocaleUsage:ERROR',
131+
'-Xep:StringCaseLocaleUsageMethodRef:ERROR',
132+
])
133+
}
134+
}
135+
}
110136

111137
task sourcesJar(type: Jar, dependsOn: classes) {
112138
classifier = "sources"

chainbase/src/main/java/org/tron/core/db/TronDatabase.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.protobuf.InvalidProtocolBufferException;
44
import java.nio.file.Paths;
5+
import java.util.Locale;
56
import java.util.Iterator;
67
import java.util.Map;
78
import java.util.Map.Entry;
@@ -37,10 +38,10 @@ protected TronDatabase(String dbName) {
3738
this.dbName = dbName;
3839

3940
if ("LEVELDB".equals(CommonParameter.getInstance().getStorage()
40-
.getDbEngine().toUpperCase())) {
41+
.getDbEngine().toUpperCase(Locale.ROOT))) {
4142
dbSource = new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(dbName), dbName);
4243
} else if ("ROCKSDB".equals(CommonParameter.getInstance()
43-
.getStorage().getDbEngine().toUpperCase())) {
44+
.getStorage().getDbEngine().toUpperCase(Locale.ROOT))) {
4445
String parentName = Paths.get(StorageUtils.getOutputDirectoryByDbName(dbName),
4546
CommonParameter.getInstance().getStorage().getDbDirectory()).toString();
4647
dbSource = new RocksDbDataSourceImpl(parentName, dbName);

chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.google.common.collect.Streams;
66
import com.google.common.reflect.TypeToken;
77
import java.io.IOException;
8+
import java.util.Locale;
89
import java.lang.reflect.Constructor;
910
import java.lang.reflect.InvocationTargetException;
1011
import java.nio.file.Paths;
@@ -54,10 +55,10 @@ public abstract class TronStoreWithRevoking<T extends ProtoCapsule> implements I
5455

5556
protected TronStoreWithRevoking(String dbName) {
5657
String dbEngine = CommonParameter.getInstance().getStorage().getDbEngine();
57-
if ("LEVELDB".equals(dbEngine.toUpperCase())) {
58+
if ("LEVELDB".equals(dbEngine.toUpperCase(Locale.ROOT))) {
5859
this.db = new LevelDB(
5960
new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(dbName), dbName));
60-
} else if ("ROCKSDB".equals(dbEngine.toUpperCase())) {
61+
} else if ("ROCKSDB".equals(dbEngine.toUpperCase(Locale.ROOT))) {
6162
String parentPath = Paths
6263
.get(StorageUtils.getOutputDirectoryByDbName(dbName), CommonParameter
6364
.getInstance().getStorage().getDbDirectory()).toString();

chainbase/src/main/java/org/tron/core/db2/common/TxCacheDB.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.nio.file.Paths;
2121
import java.nio.file.StandardOpenOption;
2222
import java.util.Iterator;
23+
import java.util.Locale;
2324
import java.util.Map;
2425
import java.util.Map.Entry;
2526
import java.util.Objects;
@@ -102,10 +103,10 @@ public TxCacheDB(String name, RecentTransactionStore recentTransactionStore,
102103
this.recentTransactionStore = recentTransactionStore;
103104
this.dynamicPropertiesStore = dynamicPropertiesStore;
104105
String dbEngine = CommonParameter.getInstance().getStorage().getDbEngine();
105-
if ("LEVELDB".equals(dbEngine.toUpperCase())) {
106+
if ("LEVELDB".equals(dbEngine.toUpperCase(Locale.ROOT))) {
106107
this.persistentStore = new LevelDB(
107108
new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(name), name));
108-
} else if ("ROCKSDB".equals(dbEngine.toUpperCase())) {
109+
} else if ("ROCKSDB".equals(dbEngine.toUpperCase(Locale.ROOT))) {
109110
String parentPath = Paths
110111
.get(StorageUtils.getOutputDirectoryByDbName(name), CommonParameter
111112
.getInstance().getStorage().getDbDirectory()).toString();

chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.tron.core.store;
22

33
import com.google.protobuf.ByteString;
4+
import java.util.Arrays;
5+
import java.util.Locale;
46
import java.util.Objects;
57
import org.apache.commons.lang3.ArrayUtils;
68
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,14 +16,42 @@
1416
@Component
1517
public class AccountIdIndexStore extends TronStoreWithRevoking<BytesCapsule> {
1618

19+
private static final Locale TURKISH = Locale.forLanguageTag("tr");
20+
21+
/**
22+
* True when this JVM runs under a Turkish or Azerbaijani locale,
23+
* meaning legacy data with dotless-ı keys may exist in the local database.
24+
*/
25+
private static final boolean IS_TURKISH_LIKE_LOCALE;
26+
27+
static {
28+
String lang = Locale.getDefault().getLanguage();
29+
IS_TURKISH_LIKE_LOCALE = "tr".equals(lang) || "az".equals(lang);
30+
}
31+
1732
@Autowired
1833
public AccountIdIndexStore(@Value("accountid-index") String dbName) {
1934
super(dbName);
2035
}
2136

22-
private static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
37+
public static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
2338
return ByteString
24-
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray();
39+
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(Locale.ROOT))
40+
.toByteArray();
41+
}
42+
43+
/**
44+
* Compute the legacy Turkish-locale key: uppercase with ROOT first to
45+
* normalize case, then lowercase with Turkish locale ('I' → 'ı').
46+
* This produces the same key a pre-fix tr/az node would have stored,
47+
* regardless of the input's original case.
48+
*/
49+
@SuppressWarnings("StringCaseLocaleUsage")
50+
private static byte[] getTurkishLegacyKey(byte[] bsAccountId) {
51+
String str = ByteString.copyFrom(bsAccountId).toStringUtf8();
52+
return ByteString
53+
.copyFromUtf8(str.toUpperCase(Locale.ROOT).toLowerCase(TURKISH))
54+
.toByteArray();
2555
}
2656

2757
public void put(AccountCapsule accountCapsule) {
@@ -41,6 +71,12 @@ public byte[] get(ByteString name) {
4171
public BytesCapsule get(byte[] key) {
4272
byte[] lowerCaseKey = getLowerCaseAccountId(key);
4373
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
74+
if (ArrayUtils.isEmpty(value) && IS_TURKISH_LIKE_LOCALE) {
75+
byte[] legacyKey = getTurkishLegacyKey(key);
76+
if (!Arrays.equals(lowerCaseKey, legacyKey)) {
77+
value = revokingDB.getUnchecked(legacyKey);
78+
}
79+
}
4480
if (ArrayUtils.isEmpty(value)) {
4581
return null;
4682
}
@@ -51,7 +87,13 @@ public BytesCapsule get(byte[] key) {
5187
public boolean has(byte[] key) {
5288
byte[] lowerCaseKey = getLowerCaseAccountId(key);
5389
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
90+
if (ArrayUtils.isEmpty(value) && IS_TURKISH_LIKE_LOCALE) {
91+
byte[] legacyKey = getTurkishLegacyKey(key);
92+
if (!Arrays.equals(lowerCaseKey, legacyKey)) {
93+
value = revokingDB.getUnchecked(legacyKey);
94+
}
95+
}
5496
return !ArrayUtils.isEmpty(value);
5597
}
5698

57-
}
99+
}

common/src/main/java/org/tron/common/args/Account.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.protobuf.ByteString;
1919
import java.io.Serializable;
20+
import java.util.Locale;
2021
import lombok.Getter;
2122
import org.apache.commons.lang3.StringUtils;
2223
import org.tron.common.utils.ByteArray;
@@ -120,7 +121,7 @@ public boolean isAccountType(final String accountType) {
120121
return false;
121122
}
122123

123-
switch (accountType.toUpperCase()) {
124+
switch (accountType.toUpperCase(Locale.ROOT)) {
124125
case ACCOUNT_TYPE_NORMAL:
125126
case ACCOUNT_TYPE_ASSETISSUE:
126127
case ACCOUNT_TYPE_CONTRACT:
@@ -138,7 +139,7 @@ public AccountType getAccountTypeByString(final String accountType) {
138139
throw new IllegalArgumentException("Account type error: Not a Normal/AssetIssue/Contract");
139140
}
140141

141-
switch (accountType.toUpperCase()) {
142+
switch (accountType.toUpperCase(Locale.ROOT)) {
142143
case ACCOUNT_TYPE_NORMAL:
143144
return AccountType.Normal;
144145
case ACCOUNT_TYPE_ASSETISSUE:

common/src/main/java/org/tron/common/runtime/vm/DataWord.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.fasterxml.jackson.annotation.JsonCreator;
2424
import com.fasterxml.jackson.annotation.JsonValue;
2525
import java.math.BigInteger;
26+
import java.util.Locale;
2627
import java.nio.ByteBuffer;
2728
import org.bouncycastle.util.Arrays;
2829
import org.bouncycastle.util.encoders.Hex;
@@ -121,7 +122,7 @@ public static boolean isZero(byte[] data) {
121122

122123
public static String shortHex(byte[] data) {
123124
byte[] bytes = ByteUtil.stripLeadingZeroes(data);
124-
String hexValue = Hex.toHexString(bytes).toUpperCase();
125+
String hexValue = Hex.toHexString(bytes).toUpperCase(Locale.ROOT);
125126
return "0x" + hexValue.replaceFirst("^0+(?!$)", "");
126127
}
127128

@@ -451,7 +452,7 @@ public String toPrefixString() {
451452
}
452453

453454
public String shortHex() {
454-
String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase();
455+
String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase(Locale.ROOT);
455456
return "0x" + hexValue.replaceFirst("^0+(?!$)", "");
456457
}
457458

errorprone/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
if (!JavaVersion.current().isJava11Compatible()) {
2+
// ErrorProne core requires JDK 11+; skip this module on JDK 8
3+
tasks.withType(JavaCompile).configureEach { enabled = false }
4+
tasks.withType(Jar).configureEach { enabled = false }
5+
} else {
6+
dependencies {
7+
compileOnly "com.google.errorprone:error_prone_annotations:${errorproneVersion}"
8+
compileOnly "com.google.errorprone:error_prone_check_api:${errorproneVersion}"
9+
compileOnly "com.google.errorprone:error_prone_core:${errorproneVersion}"
10+
compileOnly "com.google.auto.service:auto-service:1.1.1"
11+
annotationProcessor "com.google.auto.service:auto-service:1.1.1"
12+
}
13+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package errorprone;
2+
3+
import com.google.auto.service.AutoService;
4+
import com.google.errorprone.BugPattern;
5+
import com.google.errorprone.VisitorState;
6+
import com.google.errorprone.bugpatterns.BugChecker;
7+
import com.google.errorprone.matchers.Description;
8+
import com.google.errorprone.util.ASTHelpers;
9+
import com.sun.source.tree.MemberReferenceTree;
10+
import com.sun.tools.javac.code.Symbol;
11+
import com.sun.tools.javac.code.Type;
12+
13+
/**
14+
* Flags method references {@code String::toLowerCase} and {@code String::toUpperCase}
15+
* that resolve to the no-arg overload (which uses {@code Locale.getDefault()}).
16+
*
17+
* <p>The built-in ErrorProne {@code StringCaseLocaleUsage} checker only catches
18+
* direct method invocations ({@code s.toLowerCase()}), not method references
19+
* ({@code String::toLowerCase}). This checker closes that gap.
20+
*/
21+
@AutoService(BugChecker.class)
22+
@BugPattern(
23+
name = "StringCaseLocaleUsageMethodRef",
24+
summary = "String::toLowerCase and String::toUpperCase method references use "
25+
+ "Locale.getDefault(). Replace with a lambda that specifies Locale.ROOT, "
26+
+ "e.g. s -> s.toLowerCase(Locale.ROOT).",
27+
severity = BugPattern.SeverityLevel.ERROR
28+
)
29+
public class StringCaseLocaleUsageMethodRef extends BugChecker
30+
implements BugChecker.MemberReferenceTreeMatcher {
31+
32+
@Override
33+
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
34+
String name = tree.getName().toString();
35+
if (!"toLowerCase".equals(name) && !"toUpperCase".equals(name)) {
36+
return Description.NO_MATCH;
37+
}
38+
// Verify the qualifier type is java.lang.String
39+
Type qualifierType = ((com.sun.tools.javac.tree.JCTree) tree.getQualifierExpression())
40+
.type;
41+
if (qualifierType == null) {
42+
return Description.NO_MATCH;
43+
}
44+
if (!state.getTypes().isSameType(
45+
qualifierType, state.getSymtab().stringType)) {
46+
return Description.NO_MATCH;
47+
}
48+
// Only flag the no-arg overload; the Locale-taking overload is safe
49+
Symbol sym = ASTHelpers.getSymbol(tree);
50+
if (sym instanceof Symbol.MethodSymbol
51+
&& ((Symbol.MethodSymbol) sym).getParameters().isEmpty()) {
52+
return describeMatch(tree);
53+
}
54+
return Description.NO_MATCH;
55+
}
56+
}

0 commit comments

Comments
 (0)