Skip to content

Commit e00d67d

Browse files
halibobo1205claude
andcommitted
fix(consensus): use Locale.ROOT for case-insensitive 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 different index keys on tr/az nodes → potential consensus split. Changes: - Fix all toLowerCase()/toUpperCase() calls to use Locale.ROOT - Enable the ErrorProne StringCaseLocaleUsage checker at ERROR level to prevent future regressions at compile time - Add dual Turkish legacy fallback in AccountIdIndexStore for backward compatibility with keys written under tr/az locale: 1. Direct probe: toLowerCase(TURKISH) for same-case queries 2. Normalized probe: replace 'i' with 'ı' for cross-case queries - Add one-time data migration (MigrateTurkishKeyHelper) to normalize all Turkish legacy keys (ı → i) at startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1ae9bb3 commit e00d67d

30 files changed

Lines changed: 1047 additions & 55 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();
Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.tron.core.store;
22

33
import com.google.protobuf.ByteString;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.Arrays;
6+
import java.util.Locale;
47
import java.util.Objects;
8+
import lombok.extern.slf4j.Slf4j;
59
import org.apache.commons.lang3.ArrayUtils;
610
import org.springframework.beans.factory.annotation.Autowired;
711
import org.springframework.beans.factory.annotation.Value;
@@ -10,48 +14,129 @@
1014
import org.tron.core.capsule.BytesCapsule;
1115
import org.tron.core.db.TronStoreWithRevoking;
1216

13-
//todo : need Compatibility test
17+
@Slf4j(topic = "DB")
1418
@Component
1519
public class AccountIdIndexStore extends TronStoreWithRevoking<BytesCapsule> {
1620

21+
/**
22+
* Turkish dotless-ı (U+0131). On Turkish/Azerbaijani locales,
23+
* {@code 'I'.toLowerCase()} produces this instead of ASCII {@code 'i'}.
24+
* This is the ONLY ASCII letter that differs between ROOT and Turkish
25+
* {@code toLowerCase()} — verified by testTurkishLowerCaseDiffForAllAsciiLetters.
26+
*/
27+
private static final char DOTLESS_I = '\u0131'; // ı Turkish dotless-i
28+
private static final Locale TURKISH = Locale.forLanguageTag("tr");
29+
1730
@Autowired
1831
public AccountIdIndexStore(@Value("accountid-index") String dbName) {
1932
super(dbName);
2033
}
2134

22-
private static byte[] getLowerCaseAccountId(byte[] bsAccountId) {
35+
public static byte[] getLowerCaseAccountId(byte[] accountId) {
2336
return ByteString
24-
.copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray();
37+
.copyFromUtf8(ByteString.copyFrom(accountId).toStringUtf8().toLowerCase(Locale.ROOT))
38+
.toByteArray();
39+
}
40+
41+
/**
42+
* Turkish direct key: {@code toLowerCase(TURKISH)} on the original input.
43+
* Reproduces the exact key a Turkish node stored for the same-case input.
44+
* Handles lookups where query case matches the original accountId case.
45+
*
46+
* <p>Example: input "AiBI" → "aibı" (lowercase 'i' stays, uppercase 'I' → 'ı').
47+
*/
48+
@SuppressWarnings("StringCaseLocaleUsage")
49+
private static byte[] getTurkishDirectKey(byte[] accountId) {
50+
String str = ByteString.copyFrom(accountId).toStringUtf8();
51+
return ByteString.copyFromUtf8(str.toLowerCase(TURKISH)).toByteArray();
52+
}
53+
54+
/**
55+
* Turkish normalized key: ROOT key with all {@code 'i'} replaced by {@code 'ı'}.
56+
* Handles cross-case lookups (e.g., lowercase query for an accountId that
57+
* was originally uppercase on a Turkish node).
58+
*
59+
* <p>Example: rootKey "aibi" → "aıbı".
60+
*
61+
* @param rootKey the already-computed ROOT-lowered key
62+
* @return the normalized key, or {@code rootKey} itself if no 'i' is present
63+
*/
64+
private static byte[] getTurkishNormalizedKey(byte[] rootKey) {
65+
String str = new String(rootKey, StandardCharsets.UTF_8);
66+
if (str.indexOf('i') < 0) {
67+
return rootKey;
68+
}
69+
return str.replace('i', DOTLESS_I).getBytes(StandardCharsets.UTF_8);
2570
}
2671

2772
public void put(AccountCapsule accountCapsule) {
2873
byte[] lowerCaseAccountId = getLowerCaseAccountId(accountCapsule.getAccountId().toByteArray());
2974
super.put(lowerCaseAccountId, new BytesCapsule(accountCapsule.getAddress().toByteArray()));
3075
}
3176

32-
public byte[] get(ByteString name) {
33-
BytesCapsule bytesCapsule = get(name.toByteArray());
77+
public byte[] get(ByteString accountId) {
78+
BytesCapsule bytesCapsule = get(accountId.toByteArray());
3479
if (Objects.nonNull(bytesCapsule)) {
3580
return bytesCapsule.getData();
3681
}
3782
return null;
3883
}
3984

85+
/**
86+
* Look up by the standard (Locale.ROOT) accountId first; on miss, fall back to
87+
* Turkish legacy keys. The fallback covers nodes that previously ran under
88+
* tr/az locale and wrote keys containing dotless-ı (U+0131).
89+
*
90+
* <p>Two fallback probes are used:
91+
* <ol>
92+
* <li><b>Direct</b>: {@code toLowerCase(TURKISH)} — matches when query
93+
* case equals original accountId case (handles mixed 'i'/'I').</li>
94+
* <li><b>Normalized</b>: ROOT accountId with all 'i' → 'ı' — matches when
95+
* query case differs from original (e.g., all-lowercase query for
96+
* an all-uppercase stored accountId).</li>
97+
* </ol>
98+
*
99+
* <p>Each probe is skipped when it produces the same accountId as the ROOT accountId
100+
* (i.e., input contains no 'I' or 'i'). AccountIdIndexStore is a small
101+
* dataset, so the overhead of up to two extra lookups is negligible.
102+
*/
40103
@Override
41-
public BytesCapsule get(byte[] key) {
42-
byte[] lowerCaseKey = getLowerCaseAccountId(key);
43-
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
44-
if (ArrayUtils.isEmpty(value)) {
45-
return null;
46-
}
47-
return new BytesCapsule(value);
104+
public BytesCapsule get(byte[] accountId) {
105+
byte[] value = lookupWithFallback(accountId);
106+
return ArrayUtils.isEmpty(value) ? null : new BytesCapsule(value);
48107
}
49108

109+
/** See {@link #get(byte[])} for fallback strategy. */
50110
@Override
51-
public boolean has(byte[] key) {
52-
byte[] lowerCaseKey = getLowerCaseAccountId(key);
53-
byte[] value = revokingDB.getUnchecked(lowerCaseKey);
54-
return !ArrayUtils.isEmpty(value);
111+
public boolean has(byte[] accountId) {
112+
return !ArrayUtils.isEmpty(lookupWithFallback(accountId));
113+
}
114+
115+
private byte[] lookupWithFallback(byte[] accountId) {
116+
byte[] rootLocaleKey = getLowerCaseAccountId(accountId);
117+
byte[] value = revokingDB.getUnchecked(rootLocaleKey);
118+
// Fallback 1: direct Turkish accountId (same-case match).
119+
// Needed for accountIds containing BOTH 'i' and 'I' (e.g., "AiBI").
120+
// A Turkish node stored toLowerCase(TURKISH) = "aibı" — only the
121+
// direct probe reproduces this mixed 'i'/'ı' key correctly.
122+
// The normalized probe (Fallback 2) would produce "aıbı" instead.
123+
if (ArrayUtils.isEmpty(value)) {
124+
byte[] directKey = getTurkishDirectKey(accountId);
125+
if (!Arrays.equals(rootLocaleKey, directKey)) {
126+
value = revokingDB.getUnchecked(directKey);
127+
}
128+
}
129+
// Fallback 2: normalized Turkish accountId (cross-case match).
130+
// Handles queries where case differs from the original accountId,
131+
// e.g., lowercase "aibi" looking up an entry stored as "AIBI"
132+
// on a Turkish node (stored key = "aıbı").
133+
if (ArrayUtils.isEmpty(value)) {
134+
byte[] normalizedKey = getTurkishNormalizedKey(rootLocaleKey);
135+
if (!Arrays.equals(rootLocaleKey, normalizedKey)) {
136+
value = revokingDB.getUnchecked(normalizedKey);
137+
}
138+
}
139+
return value;
55140
}
56141

57-
}
142+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking<BytesCapsule>
240240

241241
private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes();
242242

243+
private static final byte[] TURKISH_KEY_MIGRATION_DONE =
244+
"TURKISH_KEY_MIGRATION_DONE".getBytes();
245+
243246
@Autowired
244247
private DynamicPropertiesStore(@Value("properties") String dbName) {
245248
super(dbName);
@@ -2993,6 +2996,18 @@ public void saveAllowTvmOsaka(long value) {
29932996
this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value)));
29942997
}
29952998

2999+
public void saveTurkishKeyMigrationDone(long num) {
3000+
this.put(TURKISH_KEY_MIGRATION_DONE,
3001+
new BytesCapsule(ByteArray.fromLong(num)));
3002+
}
3003+
3004+
public long getTurkishKeyMigrationDone() {
3005+
return Optional.ofNullable(getUnchecked(TURKISH_KEY_MIGRATION_DONE))
3006+
.map(BytesCapsule::getData)
3007+
.map(ByteArray::toLong)
3008+
.orElse(0L);
3009+
}
3010+
29963011
private static class DynamicResourceProperties {
29973012

29983013
private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes();

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+
}

0 commit comments

Comments
 (0)