Skip to content

Commit 537cd1d

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 537cd1d

25 files changed

Lines changed: 476 additions & 35 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' && javaVersion.isJava11Compatible()) {
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: 58 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,30 @@
1416
@Component
1517
public class AccountIdIndexStore extends TronStoreWithRevoking<BytesCapsule> {
1618

19+
private static final Locale TURKISH = Locale.forLanguageTag("tr");
20+
1721
@Autowired
1822
public AccountIdIndexStore(@Value("accountid-index") String dbName) {
1923
super(dbName);
2024
}
2125

22-
private static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
26+
public static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
27+
return ByteString
28+
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(Locale.ROOT))
29+
.toByteArray();
30+
}
31+
32+
/**
33+
* Compute the legacy Turkish-locale lower-case key.
34+
* On pre-fix tr/az nodes, 'I' (U+0049) was folded to 'ı' (U+0131).
35+
* We use an explicit Turkish locale so that ALL nodes (not just tr/az)
36+
* can probe for legacy keys written by former tr/az nodes.
37+
*/
38+
@SuppressWarnings("StringCaseLocaleUsage")
39+
private static byte[] getTurkishLowerCaseAccountId(byte[] bsAccountId) {
2340
return ByteString
24-
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray();
41+
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(TURKISH))
42+
.toByteArray();
2543
}
2644

2745
public void put(AccountCapsule accountCapsule) {
@@ -41,6 +59,9 @@ public byte[] get(ByteString name) {
4159
public BytesCapsule get(byte[] key) {
4260
byte[] lowerCaseKey = getLowerCaseAccountId(key);
4361
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
62+
if (ArrayUtils.isEmpty(value)) {
63+
value = probeTurkishLegacyKey(key, lowerCaseKey);
64+
}
4465
if (ArrayUtils.isEmpty(value)) {
4566
return null;
4667
}
@@ -51,7 +72,41 @@ public BytesCapsule get(byte[] key) {
5172
public boolean has(byte[] key) {
5273
byte[] lowerCaseKey = getLowerCaseAccountId(key);
5374
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
75+
if (ArrayUtils.isEmpty(value)) {
76+
value = probeTurkishLegacyKey(key, lowerCaseKey);
77+
}
5478
return !ArrayUtils.isEmpty(value);
5579
}
5680

57-
}
81+
/**
82+
* Probe for a legacy key that may have been written by a tr/az locale node.
83+
*
84+
* <p>The input {@code key} might be in any case. To find a legacy entry we need
85+
* to reconstruct the key that a tr/az node would have stored. Since Turkish
86+
* toLowerCase only differs for UPPERCASE 'I' → 'ı', we first uppercase the
87+
* input with Locale.ROOT (so any 'i' becomes 'I'), then lowercase with
88+
* Turkish locale (so 'I' → 'ı'), producing the legacy key regardless of the
89+
* original case of the input.</p>
90+
*/
91+
private byte[] probeTurkishLegacyKey(byte[] key, byte[] standardLowerKey) {
92+
// Direct Turkish lower-case of the original input
93+
byte[] turkishKey = getTurkishLowerCaseAccountId(key);
94+
if (!Arrays.equals(standardLowerKey, turkishKey)) {
95+
byte[] value = revokingDB.getUnchecked(turkishKey);
96+
if (!ArrayUtils.isEmpty(value)) {
97+
return value;
98+
}
99+
}
100+
// Input may already be lowercase ('i'), which Turkish toLowerCase won't change.
101+
// Uppercase first with ROOT ('i' → 'I'), then Turkish lowercase ('I' → 'ı').
102+
String upperStr = ByteString.copyFrom(key).toStringUtf8().toUpperCase(Locale.ROOT);
103+
byte[] turkishFromUpper = ByteString
104+
.copyFromUtf8(upperStr.toLowerCase(TURKISH)).toByteArray();
105+
if (!Arrays.equals(standardLowerKey, turkishFromUpper)
106+
&& !Arrays.equals(turkishKey, turkishFromUpper)) {
107+
return revokingDB.getUnchecked(turkishFromUpper);
108+
}
109+
return null;
110+
}
111+
112+
}

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: 4 additions & 3 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;
@@ -892,7 +893,7 @@ public static void applyConfigParams(
892893
PARAMETER.disabledApiList =
893894
config.hasPath(ConfigKey.NODE_DISABLED_API_LIST)
894895
? config.getStringList(ConfigKey.NODE_DISABLED_API_LIST)
895-
.stream().map(String::toLowerCase).collect(Collectors.toList())
896+
.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toList())
896897
: Collections.emptyList();
897898

898899
if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)) {
@@ -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)