Skip to content

Commit 05bfab2

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 05bfab2

25 files changed

Lines changed: 427 additions & 34 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: 23 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,23 @@ 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') {
117+
apply plugin: 'net.ltgt.errorprone'
118+
dependencies {
119+
errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}"
120+
}
121+
tasks.withType(JavaCompile).configureEach {
122+
options.errorprone {
123+
enabled = true
124+
disableWarningsInGeneratedCode = true
125+
disableAllChecks = true
126+
excludedPaths = '.*/generated/.*'
127+
errorproneArgs.addAll([
128+
'-Xep:StringCaseLocaleUsage:ERROR',
129+
])
130+
}
131+
}
132+
}
110133

111134
task sourcesJar(type: Jar, dependsOn: classes) {
112135
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: 34 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;
@@ -19,9 +21,32 @@ public AccountIdIndexStore(@Value("accountid-index") String dbName) {
1921
super(dbName);
2022
}
2123

22-
private static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
24+
public static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
2325
return ByteString
24-
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray();
26+
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(Locale.ROOT))
27+
.toByteArray();
28+
}
29+
30+
/**
31+
* Get the legacy lower-case key using the system default Locale.
32+
* Only differs from {@link #getLowerCaseAccountId} on tr/az locales,
33+
* where 'I' maps to 'ı' (U+0131) instead of 'i' (U+0069).
34+
*/
35+
@SuppressWarnings("StringCaseLocaleUsage")
36+
private static byte[] getLegacyLowerCaseAccountId(byte[] bsAccountId) {
37+
return ByteString
38+
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase())
39+
.toByteArray();
40+
}
41+
42+
/**
43+
* Check whether the legacy key differs from the standard key.
44+
* True only when the system Locale is tr or az and the input contains
45+
* characters affected by locale-sensitive case folding (e.g. 'I').
46+
*/
47+
private static boolean hasLegacyKey(byte[] standardKey, byte[] bsAccountId) {
48+
byte[] legacyKey = getLegacyLowerCaseAccountId(bsAccountId);
49+
return !Arrays.equals(standardKey, legacyKey);
2550
}
2651

2752
public void put(AccountCapsule accountCapsule) {
@@ -41,6 +66,9 @@ public byte[] get(ByteString name) {
4166
public BytesCapsule get(byte[] key) {
4267
byte[] lowerCaseKey = getLowerCaseAccountId(key);
4368
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
69+
if (ArrayUtils.isEmpty(value) && hasLegacyKey(lowerCaseKey, key)) {
70+
value = revokingDB.getUnchecked(getLegacyLowerCaseAccountId(key));
71+
}
4472
if (ArrayUtils.isEmpty(value)) {
4573
return null;
4674
}
@@ -51,7 +79,10 @@ public BytesCapsule get(byte[] key) {
5179
public boolean has(byte[] key) {
5280
byte[] lowerCaseKey = getLowerCaseAccountId(key);
5381
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
82+
if (ArrayUtils.isEmpty(value) && hasLegacyKey(lowerCaseKey, key)) {
83+
value = revokingDB.getUnchecked(getLegacyLowerCaseAccountId(key));
84+
}
5485
return !ArrayUtils.isEmpty(value);
5586
}
5687

57-
}
88+
}

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

framework/src/main/java/org/tron/core/Wallet.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.util.HashMap;
5858
import java.util.Iterator;
5959
import java.util.List;
60+
import java.util.Locale;
6061
import java.util.Map;
6162
import java.util.Map.Entry;
6263
import java.util.Objects;
@@ -3880,14 +3881,14 @@ private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddr
38803881
for (String topic : topicsList) {
38813882
byte[] topicHash = Hash.sha3(ByteArray.fromString(topic));
38823883
if (Arrays.equals(topicsBytes, topicHash)) {
3883-
if (topic.toLowerCase().contains("mint")) {
3884+
if (topic.toLowerCase(Locale.ROOT).contains("mint")) {
38843885
return 1;
3885-
} else if (topic.toLowerCase().contains("transfer")) {
3886+
} else if (topic.toLowerCase(Locale.ROOT).contains("transfer")) {
38863887
return 2;
3887-
} else if (topic.toLowerCase().contains("burn")) {
3888-
if (topic.toLowerCase().contains("leaf")) {
3888+
} else if (topic.toLowerCase(Locale.ROOT).contains("burn")) {
3889+
if (topic.toLowerCase(Locale.ROOT).contains("leaf")) {
38893890
return 3;
3890-
} else if (topic.toLowerCase().contains("token")) {
3891+
} else if (topic.toLowerCase(Locale.ROOT).contains("token")) {
38913892
return 4;
38923893
}
38933894
}

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.HashSet;
3636
import java.util.LinkedHashMap;
3737
import java.util.List;
38+
import java.util.Locale;
3839
import java.util.Map;
3940
import java.util.Objects;
4041
import java.util.Optional;
@@ -1770,7 +1771,7 @@ public static void printHelp(JCommander jCommander) {
17701771
Map<String, String[]> groupOptionListMap = Args.getOptionGroup();
17711772
for (Map.Entry<String, String[]> entry : groupOptionListMap.entrySet()) {
17721773
String group = entry.getKey();
1773-
helpStr.append(String.format("%n%s OPTIONS:%n", group.toUpperCase()));
1774+
helpStr.append(String.format("%n%s OPTIONS:%n", group.toUpperCase(Locale.ROOT)));
17741775
int optionMaxLength = Arrays.stream(entry.getValue()).mapToInt(p -> {
17751776
ParameterDescription tmpParameterDescription = stringParameterDescriptionMap.get(p);
17761777
if (tmpParameterDescription == null) {
@@ -1810,7 +1811,7 @@ public static String upperFirst(String name) {
18101811
if (name.length() <= 1) {
18111812
return name;
18121813
}
1813-
name = name.substring(0, 1).toUpperCase() + name.substring(1);
1814+
name = name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1);
18141815
return name;
18151816
}
18161817

0 commit comments

Comments
 (0)