From 1a0709a393b28dcb0815b2e819e3cdaadcf8cf0f Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Sun, 12 Apr 2026 15:52:23 +0800 Subject: [PATCH 1/4] fix(consensus): use Locale.ROOT for case-insensitive operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com> Co-Authored-By: codex --- .../core/actuator/AssetIssueActuator.java | 3 +- build.gradle | 26 ++ .../java/org/tron/core/db/TronDatabase.java | 5 +- .../tron/core/db/TronStoreWithRevoking.java | 5 +- .../org/tron/core/db2/common/TxCacheDB.java | 5 +- .../tron/core/store/AccountIdIndexStore.java | 119 ++++- .../core/store/DynamicPropertiesStore.java | 15 + .../java/org/tron/common/args/Account.java | 5 +- .../org/tron/common/runtime/vm/DataWord.java | 5 +- errorprone/build.gradle | 13 + .../StringCaseLocaleUsageMethodRef.java | 56 +++ .../src/main/java/org/tron/core/Wallet.java | 11 +- .../java/org/tron/core/config/args/Args.java | 7 +- .../main/java/org/tron/core/db/Manager.java | 9 + .../core/db/api/MigrateTurkishKeyHelper.java | 92 ++++ .../services/filter/HttpApiAccessFilter.java | 3 +- .../org/tron/core/services/http/Util.java | 5 +- .../ratelimiter/RpcApiAccessInterceptor.java | 3 +- .../java/org/tron/keystore/WalletUtils.java | 3 +- .../org/tron/program/KeystoreFactory.java | 3 +- .../tron/common/cron/CronExpressionTest.java | 1 + .../org/tron/common/crypto/SM2KeyTest.java | 1 + .../common/runtime/vm/BytecodeCompiler.java | 1 + .../common/runtime/vm/OperationsTest.java | 2 + .../common/utils/client/utils/DataWord.java | 1 + .../tron/core/db/AccountIdIndexStoreTest.java | 417 +++++++++++++++++- .../core/zksnark/ShieldedReceiveTest.java | 1 + gradle/verification-metadata.xml | 267 +++++++++++ .../common/org/tron/common/arch/Arch.java | 11 +- settings.gradle | 7 + 30 files changed, 1047 insertions(+), 55 deletions(-) create mode 100644 errorprone/build.gradle create mode 100644 errorprone/src/main/java/errorprone/StringCaseLocaleUsageMethodRef.java create mode 100644 framework/src/main/java/org/tron/core/db/api/MigrateTurkishKeyHelper.java diff --git a/actuator/src/main/java/org/tron/core/actuator/AssetIssueActuator.java b/actuator/src/main/java/org/tron/core/actuator/AssetIssueActuator.java index 618a9fb191e..59fa6e0aaa9 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AssetIssueActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AssetIssueActuator.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.tron.common.math.StrictMathWrapper; @@ -166,7 +167,7 @@ public boolean validate() throws ContractValidateException { } if (dynamicStore.getAllowSameTokenName() != 0) { - String name = assetIssueContract.getName().toStringUtf8().toLowerCase(); + String name = assetIssueContract.getName().toStringUtf8().toLowerCase(Locale.ROOT); if (("trx").equals(name)) { throw new ContractValidateException("assetName can't be trx"); } diff --git a/build.gradle b/build.gradle index 12a0622db99..e71dcc056cc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,16 @@ import org.gradle.nativeplatform.platform.internal.Architectures import org.gradle.internal.os.OperatingSystem + +plugins { + id 'net.ltgt.errorprone' version '5.0.0' apply false +} + allprojects { version = "1.0.0" apply plugin: "java-library" ext { springVersion = "5.3.39" + errorproneVersion = "2.42.0" } } def arch = System.getProperty("os.arch").toLowerCase() @@ -107,6 +113,26 @@ subprojects { testImplementation "org.mockito:mockito-core:4.11.0" testImplementation "org.mockito:mockito-inline:4.11.0" } + if (project.name != 'protocol' && project.name != 'errorprone' + && javaVersion.isJava11Compatible()) { + apply plugin: 'net.ltgt.errorprone' + dependencies { + errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" + errorprone rootProject.project(':errorprone') + } + tasks.withType(JavaCompile).configureEach { + options.errorprone { + enabled = true + disableWarningsInGeneratedCode = true + disableAllChecks = true + excludedPaths = '.*/generated/.*' + errorproneArgs.addAll([ + '-Xep:StringCaseLocaleUsage:ERROR', + '-Xep:StringCaseLocaleUsageMethodRef:ERROR', + ]) + } + } + } task sourcesJar(type: Jar, dependsOn: classes) { classifier = "sources" diff --git a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java index 40762568c82..3fbf4a31c34 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java @@ -2,6 +2,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import java.nio.file.Paths; +import java.util.Locale; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -37,10 +38,10 @@ protected TronDatabase(String dbName) { this.dbName = dbName; if ("LEVELDB".equals(CommonParameter.getInstance().getStorage() - .getDbEngine().toUpperCase())) { + .getDbEngine().toUpperCase(Locale.ROOT))) { dbSource = new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(dbName), dbName); } else if ("ROCKSDB".equals(CommonParameter.getInstance() - .getStorage().getDbEngine().toUpperCase())) { + .getStorage().getDbEngine().toUpperCase(Locale.ROOT))) { String parentName = Paths.get(StorageUtils.getOutputDirectoryByDbName(dbName), CommonParameter.getInstance().getStorage().getDbDirectory()).toString(); dbSource = new RocksDbDataSourceImpl(parentName, dbName); diff --git a/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java b/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java index 73b1b103d76..0911ec658b4 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java +++ b/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java @@ -5,6 +5,7 @@ import com.google.common.collect.Streams; import com.google.common.reflect.TypeToken; import java.io.IOException; +import java.util.Locale; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.file.Paths; @@ -54,10 +55,10 @@ public abstract class TronStoreWithRevoking implements I protected TronStoreWithRevoking(String dbName) { String dbEngine = CommonParameter.getInstance().getStorage().getDbEngine(); - if ("LEVELDB".equals(dbEngine.toUpperCase())) { + if ("LEVELDB".equals(dbEngine.toUpperCase(Locale.ROOT))) { this.db = new LevelDB( new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(dbName), dbName)); - } else if ("ROCKSDB".equals(dbEngine.toUpperCase())) { + } else if ("ROCKSDB".equals(dbEngine.toUpperCase(Locale.ROOT))) { String parentPath = Paths .get(StorageUtils.getOutputDirectoryByDbName(dbName), CommonParameter .getInstance().getStorage().getDbDirectory()).toString(); diff --git a/chainbase/src/main/java/org/tron/core/db2/common/TxCacheDB.java b/chainbase/src/main/java/org/tron/core/db2/common/TxCacheDB.java index 31131de0866..e545f560830 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/TxCacheDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/TxCacheDB.java @@ -20,6 +20,7 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -102,10 +103,10 @@ public TxCacheDB(String name, RecentTransactionStore recentTransactionStore, this.recentTransactionStore = recentTransactionStore; this.dynamicPropertiesStore = dynamicPropertiesStore; String dbEngine = CommonParameter.getInstance().getStorage().getDbEngine(); - if ("LEVELDB".equals(dbEngine.toUpperCase())) { + if ("LEVELDB".equals(dbEngine.toUpperCase(Locale.ROOT))) { this.persistentStore = new LevelDB( new LevelDbDataSourceImpl(StorageUtils.getOutputDirectoryByDbName(name), name)); - } else if ("ROCKSDB".equals(dbEngine.toUpperCase())) { + } else if ("ROCKSDB".equals(dbEngine.toUpperCase(Locale.ROOT))) { String parentPath = Paths .get(StorageUtils.getOutputDirectoryByDbName(name), CommonParameter .getInstance().getStorage().getDbDirectory()).toString(); diff --git a/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java b/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java index 1a695c5f627..9aad6992d27 100644 --- a/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java +++ b/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java @@ -1,7 +1,11 @@ package org.tron.core.store; import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Locale; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -10,18 +14,59 @@ import org.tron.core.capsule.BytesCapsule; import org.tron.core.db.TronStoreWithRevoking; -//todo : need Compatibility test +@Slf4j(topic = "DB") @Component public class AccountIdIndexStore extends TronStoreWithRevoking { + /** + * Turkish dotless-ı (U+0131). On Turkish/Azerbaijani locales, + * {@code 'I'.toLowerCase()} produces this instead of ASCII {@code 'i'}. + * This is the ONLY ASCII letter that differs between ROOT and Turkish + * {@code toLowerCase()} — verified by testTurkishLowerCaseDiffForAllAsciiLetters. + */ + private static final char DOTLESS_I = '\u0131'; // ı Turkish dotless-i + private static final Locale TURKISH = Locale.forLanguageTag("tr"); + @Autowired public AccountIdIndexStore(@Value("accountid-index") String dbName) { super(dbName); } - private static byte[] getLowerCaseAccountId(byte[] bsAccountId) { + public static byte[] getLowerCaseAccountId(byte[] accountId) { return ByteString - .copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray(); + .copyFromUtf8(ByteString.copyFrom(accountId).toStringUtf8().toLowerCase(Locale.ROOT)) + .toByteArray(); + } + + /** + * Turkish direct key: {@code toLowerCase(TURKISH)} on the original input. + * Reproduces the exact key a Turkish node stored for the same-case input. + * Handles lookups where query case matches the original accountId case. + * + *

Example: input "AiBI" → "aibı" (lowercase 'i' stays, uppercase 'I' → 'ı'). + */ + @SuppressWarnings("StringCaseLocaleUsage") + private static byte[] getTurkishDirectKey(byte[] accountId) { + String str = ByteString.copyFrom(accountId).toStringUtf8(); + return ByteString.copyFromUtf8(str.toLowerCase(TURKISH)).toByteArray(); + } + + /** + * Turkish normalized key: ROOT key with all {@code 'i'} replaced by {@code 'ı'}. + * Handles cross-case lookups (e.g., lowercase query for an accountId that + * was originally uppercase on a Turkish node). + * + *

Example: rootKey "aibi" → "aıbı". + * + * @param rootKey the already-computed ROOT-lowered key + * @return the normalized key, or {@code rootKey} itself if no 'i' is present + */ + private static byte[] getTurkishNormalizedKey(byte[] rootKey) { + String str = new String(rootKey, StandardCharsets.UTF_8); + if (str.indexOf('i') < 0) { + return rootKey; + } + return str.replace('i', DOTLESS_I).getBytes(StandardCharsets.UTF_8); } public void put(AccountCapsule accountCapsule) { @@ -29,29 +74,69 @@ public void put(AccountCapsule accountCapsule) { super.put(lowerCaseAccountId, new BytesCapsule(accountCapsule.getAddress().toByteArray())); } - public byte[] get(ByteString name) { - BytesCapsule bytesCapsule = get(name.toByteArray()); + public byte[] get(ByteString accountId) { + BytesCapsule bytesCapsule = get(accountId.toByteArray()); if (Objects.nonNull(bytesCapsule)) { return bytesCapsule.getData(); } return null; } + /** + * Look up by the standard (Locale.ROOT) accountId first; on miss, fall back to + * Turkish legacy keys. The fallback covers nodes that previously ran under + * tr/az locale and wrote keys containing dotless-ı (U+0131). + * + *

Two fallback probes are used: + *

    + *
  1. Direct: {@code toLowerCase(TURKISH)} — matches when query + * case equals original accountId case (handles mixed 'i'/'I').
  2. + *
  3. Normalized: ROOT accountId with all 'i' → 'ı' — matches when + * query case differs from original (e.g., all-lowercase query for + * an all-uppercase stored accountId).
  4. + *
+ * + *

Each probe is skipped when it produces the same accountId as the ROOT accountId + * (i.e., input contains no 'I' or 'i'). AccountIdIndexStore is a small + * dataset, so the overhead of up to two extra lookups is negligible. + */ @Override - public BytesCapsule get(byte[] key) { - byte[] lowerCaseKey = getLowerCaseAccountId(key); - byte[] value = revokingDB.getUnchecked(lowerCaseKey); - if (ArrayUtils.isEmpty(value)) { - return null; - } - return new BytesCapsule(value); + public BytesCapsule get(byte[] accountId) { + byte[] value = lookupWithFallback(accountId); + return ArrayUtils.isEmpty(value) ? null : new BytesCapsule(value); } + /** See {@link #get(byte[])} for fallback strategy. */ @Override - public boolean has(byte[] key) { - byte[] lowerCaseKey = getLowerCaseAccountId(key); - byte[] value = revokingDB.getUnchecked(lowerCaseKey); - return !ArrayUtils.isEmpty(value); + public boolean has(byte[] accountId) { + return !ArrayUtils.isEmpty(lookupWithFallback(accountId)); + } + + private byte[] lookupWithFallback(byte[] accountId) { + byte[] rootLocaleKey = getLowerCaseAccountId(accountId); + byte[] value = revokingDB.getUnchecked(rootLocaleKey); + // Fallback 1: direct Turkish accountId (same-case match). + // Needed for accountIds containing BOTH 'i' and 'I' (e.g., "AiBI"). + // A Turkish node stored toLowerCase(TURKISH) = "aibı" — only the + // direct probe reproduces this mixed 'i'/'ı' key correctly. + // The normalized probe (Fallback 2) would produce "aıbı" instead. + if (ArrayUtils.isEmpty(value)) { + byte[] directKey = getTurkishDirectKey(accountId); + if (!Arrays.equals(rootLocaleKey, directKey)) { + value = revokingDB.getUnchecked(directKey); + } + } + // Fallback 2: normalized Turkish accountId (cross-case match). + // Handles queries where case differs from the original accountId, + // e.g., lowercase "aibi" looking up an entry stored as "AIBI" + // on a Turkish node (stored key = "aıbı"). + if (ArrayUtils.isEmpty(value)) { + byte[] normalizedKey = getTurkishNormalizedKey(rootLocaleKey); + if (!Arrays.equals(rootLocaleKey, normalizedKey)) { + value = revokingDB.getUnchecked(normalizedKey); + } + } + return value; } -} \ No newline at end of file +} diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index e0adb0d444a..e74e84f736d 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] TURKISH_KEY_MIGRATION_DONE = + "TURKISH_KEY_MIGRATION_DONE".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2996,18 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public void saveTurkishKeyMigrationDone(long num) { + this.put(TURKISH_KEY_MIGRATION_DONE, + new BytesCapsule(ByteArray.fromLong(num))); + } + + public long getTurkishKeyMigrationDone() { + return Optional.ofNullable(getUnchecked(TURKISH_KEY_MIGRATION_DONE)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); diff --git a/common/src/main/java/org/tron/common/args/Account.java b/common/src/main/java/org/tron/common/args/Account.java index 872d202f86e..bbaaf9d1249 100644 --- a/common/src/main/java/org/tron/common/args/Account.java +++ b/common/src/main/java/org/tron/common/args/Account.java @@ -17,6 +17,7 @@ import com.google.protobuf.ByteString; import java.io.Serializable; +import java.util.Locale; import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.tron.common.utils.ByteArray; @@ -120,7 +121,7 @@ public boolean isAccountType(final String accountType) { return false; } - switch (accountType.toUpperCase()) { + switch (accountType.toUpperCase(Locale.ROOT)) { case ACCOUNT_TYPE_NORMAL: case ACCOUNT_TYPE_ASSETISSUE: case ACCOUNT_TYPE_CONTRACT: @@ -138,7 +139,7 @@ public AccountType getAccountTypeByString(final String accountType) { throw new IllegalArgumentException("Account type error: Not a Normal/AssetIssue/Contract"); } - switch (accountType.toUpperCase()) { + switch (accountType.toUpperCase(Locale.ROOT)) { case ACCOUNT_TYPE_NORMAL: return AccountType.Normal; case ACCOUNT_TYPE_ASSETISSUE: diff --git a/common/src/main/java/org/tron/common/runtime/vm/DataWord.java b/common/src/main/java/org/tron/common/runtime/vm/DataWord.java index faeae45782e..79db64c88cc 100644 --- a/common/src/main/java/org/tron/common/runtime/vm/DataWord.java +++ b/common/src/main/java/org/tron/common/runtime/vm/DataWord.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import java.math.BigInteger; +import java.util.Locale; import java.nio.ByteBuffer; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; @@ -121,7 +122,7 @@ public static boolean isZero(byte[] data) { public static String shortHex(byte[] data) { byte[] bytes = ByteUtil.stripLeadingZeroes(data); - String hexValue = Hex.toHexString(bytes).toUpperCase(); + String hexValue = Hex.toHexString(bytes).toUpperCase(Locale.ROOT); return "0x" + hexValue.replaceFirst("^0+(?!$)", ""); } @@ -451,7 +452,7 @@ public String toPrefixString() { } public String shortHex() { - String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase(); + String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase(Locale.ROOT); return "0x" + hexValue.replaceFirst("^0+(?!$)", ""); } diff --git a/errorprone/build.gradle b/errorprone/build.gradle new file mode 100644 index 00000000000..f8a634b7edc --- /dev/null +++ b/errorprone/build.gradle @@ -0,0 +1,13 @@ +if (!JavaVersion.current().isJava11Compatible()) { + // ErrorProne core requires JDK 11+; skip this module on JDK 8 + tasks.withType(JavaCompile).configureEach { enabled = false } + tasks.withType(Jar).configureEach { enabled = false } +} else { + dependencies { + compileOnly "com.google.errorprone:error_prone_annotations:${errorproneVersion}" + compileOnly "com.google.errorprone:error_prone_check_api:${errorproneVersion}" + compileOnly "com.google.errorprone:error_prone_core:${errorproneVersion}" + compileOnly "com.google.auto.service:auto-service:1.1.1" + annotationProcessor "com.google.auto.service:auto-service:1.1.1" + } +} diff --git a/errorprone/src/main/java/errorprone/StringCaseLocaleUsageMethodRef.java b/errorprone/src/main/java/errorprone/StringCaseLocaleUsageMethodRef.java new file mode 100644 index 00000000000..1486d945b58 --- /dev/null +++ b/errorprone/src/main/java/errorprone/StringCaseLocaleUsageMethodRef.java @@ -0,0 +1,56 @@ +package errorprone; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Type; + +/** + * Flags method references {@code String::toLowerCase} and {@code String::toUpperCase} + * that resolve to the no-arg overload (which uses {@code Locale.getDefault()}). + * + *

The built-in ErrorProne {@code StringCaseLocaleUsage} checker only catches + * direct method invocations ({@code s.toLowerCase()}), not method references + * ({@code String::toLowerCase}). This checker closes that gap. + */ +@AutoService(BugChecker.class) +@BugPattern( + name = "StringCaseLocaleUsageMethodRef", + summary = "String::toLowerCase and String::toUpperCase method references use " + + "Locale.getDefault(). Replace with a lambda that specifies Locale.ROOT, " + + "e.g. s -> s.toLowerCase(Locale.ROOT).", + severity = BugPattern.SeverityLevel.ERROR +) +public class StringCaseLocaleUsageMethodRef extends BugChecker + implements BugChecker.MemberReferenceTreeMatcher { + + @Override + public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { + String name = tree.getName().toString(); + if (!"toLowerCase".equals(name) && !"toUpperCase".equals(name)) { + return Description.NO_MATCH; + } + // Verify the qualifier type is java.lang.String + Type qualifierType = ((com.sun.tools.javac.tree.JCTree) tree.getQualifierExpression()) + .type; + if (qualifierType == null) { + return Description.NO_MATCH; + } + if (!state.getTypes().isSameType( + qualifierType, state.getSymtab().stringType)) { + return Description.NO_MATCH; + } + // Only flag the no-arg overload; the Locale-taking overload is safe + Symbol sym = ASTHelpers.getSymbol(tree); + if (sym instanceof Symbol.MethodSymbol + && ((Symbol.MethodSymbol) sym).getParameters().isEmpty()) { + return describeMatch(tree); + } + return Description.NO_MATCH; + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..51c9041e08b 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -57,6 +57,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -3880,14 +3881,14 @@ private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddr for (String topic : topicsList) { byte[] topicHash = Hash.sha3(ByteArray.fromString(topic)); if (Arrays.equals(topicsBytes, topicHash)) { - if (topic.toLowerCase().contains("mint")) { + if (topic.toLowerCase(Locale.ROOT).contains("mint")) { return 1; - } else if (topic.toLowerCase().contains("transfer")) { + } else if (topic.toLowerCase(Locale.ROOT).contains("transfer")) { return 2; - } else if (topic.toLowerCase().contains("burn")) { - if (topic.toLowerCase().contains("leaf")) { + } else if (topic.toLowerCase(Locale.ROOT).contains("burn")) { + if (topic.toLowerCase(Locale.ROOT).contains("leaf")) { return 3; - } else if (topic.toLowerCase().contains("token")) { + } else if (topic.toLowerCase(Locale.ROOT).contains("token")) { return 4; } } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 83d7fd2c63d..0dd0b2811e5 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -892,7 +893,7 @@ public static void applyConfigParams( PARAMETER.disabledApiList = config.hasPath(ConfigKey.NODE_DISABLED_API_LIST) ? config.getStringList(ConfigKey.NODE_DISABLED_API_LIST) - .stream().map(String::toLowerCase).collect(Collectors.toList()) + .stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toList()) : Collections.emptyList(); if (config.hasPath(ConfigKey.NODE_SHUTDOWN_BLOCK_TIME)) { @@ -1770,7 +1771,7 @@ public static void printHelp(JCommander jCommander) { Map groupOptionListMap = Args.getOptionGroup(); for (Map.Entry entry : groupOptionListMap.entrySet()) { String group = entry.getKey(); - helpStr.append(String.format("%n%s OPTIONS:%n", group.toUpperCase())); + helpStr.append(String.format("%n%s OPTIONS:%n", group.toUpperCase(Locale.ROOT))); int optionMaxLength = Arrays.stream(entry.getValue()).mapToInt(p -> { ParameterDescription tmpParameterDescription = stringParameterDescriptionMap.get(p); if (tmpParameterDescription == null) { @@ -1810,7 +1811,7 @@ public static String upperFirst(String name) { if (name.length() <= 1) { return name; } - name = name.substring(0, 1).toUpperCase() + name.substring(1); + name = name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1); return name; } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index cd1a61c01fe..851db8e6d19 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -109,6 +109,7 @@ import org.tron.core.db.api.AssetUpdateHelper; import org.tron.core.db.api.BandwidthPriceHistoryLoader; import org.tron.core.db.api.EnergyPriceHistoryLoader; +import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; import org.tron.core.db2.core.Chainbase; @@ -372,6 +373,10 @@ public boolean needToSetBlackholePermission() { return getDynamicPropertiesStore().getSetBlackholeAccountPermission() == 0L; } + private boolean needToMigrateTurkishKeys() { + return getDynamicPropertiesStore().getTurkishKeyMigrationDone() == 0L; + } + private void resetBlackholeAccountPermission() { AccountCapsule blackholeAccount = getAccountStore().getBlackhole(); @@ -542,6 +547,10 @@ public void init() { resetBlackholeAccountPermission(); } + if (needToMigrateTurkishKeys()) { + new MigrateTurkishKeyHelper(chainBaseManager).doWork(); + } + //for test only chainBaseManager.getDynamicPropertiesStore().updateDynamicStoreByConfig(); diff --git a/framework/src/main/java/org/tron/core/db/api/MigrateTurkishKeyHelper.java b/framework/src/main/java/org/tron/core/db/api/MigrateTurkishKeyHelper.java new file mode 100644 index 00000000000..46c3d367ece --- /dev/null +++ b/framework/src/main/java/org/tron/core/db/api/MigrateTurkishKeyHelper.java @@ -0,0 +1,92 @@ +package org.tron.core.db.api; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.tron.core.ChainBaseManager; +import org.tron.core.db2.common.IRevokingDB; +import org.tron.core.store.AccountIdIndexStore; + +/** + * One-time migration: normalize any Turkish legacy keys (containing + * dotless-ı U+0131) to ROOT keys (with ASCII 'i') in AccountIdIndexStore. + * + *

On Turkish/Azerbaijani locales, {@code String.toLowerCase()} maps + * uppercase 'I' to dotless-ı instead of 'i'. Nodes that ran under such + * locales wrote different index keys, causing lookup failures. + * This migration ensures all nodes have identical DB state regardless + * of their locale history. + * + *

Called from {@code Manager.init()} via the standard + * {@code DynamicPropertiesStore} flag pattern. + * + * @see AccountIdIndexStore + */ +@Slf4j(topic = "DB") +public class MigrateTurkishKeyHelper { + + private static final char DOTLESS_I = '\u0131'; // ı Turkish dotless-i + + private final ChainBaseManager chainBaseManager; + + public MigrateTurkishKeyHelper(ChainBaseManager chainBaseManager) { + this.chainBaseManager = chainBaseManager; + } + + /** + * Scan AccountIdIndexStore for keys containing Turkish dotless-ı (U+0131), + * replace them with ROOT-equivalent keys (ı → i), and delete the old keys. + * + *

Uses raw {@link IRevokingDB} access to bypass the fallback lookup + * logic in {@link AccountIdIndexStore#has(byte[])} — we need exact-match + * checks to determine whether the ROOT key already exists in the DB. + */ + public void doWork() { + long start = System.currentTimeMillis(); + logger.info("Start to migrate Turkish legacy keys in AccountIdIndexStore"); + + final IRevokingDB revokingDB = chainBaseManager.getAccountIdIndexStore() + .getRevokingDB(); + long totalKeys = 0; + List keysToDelete = new ArrayList<>(); + List> entriesToMigrate = new ArrayList<>(); + + // Phase 1: scan for keys containing 'ı' (U+0131) + for (Map.Entry entry : revokingDB) { + totalKeys++; + String keyStr = new String(entry.getKey(), StandardCharsets.UTF_8); + if (keyStr.indexOf(DOTLESS_I) >= 0) { + entriesToMigrate.add(entry); + } + } + + // Phase 2: migrate each Turkish key to its ROOT equivalent + for (Map.Entry entry : entriesToMigrate) { + String keyStr = new String(entry.getKey(), StandardCharsets.UTF_8); + byte[] rootKey = keyStr.replace(DOTLESS_I, 'i') + .getBytes(StandardCharsets.UTF_8); + // Only write if ROOT key doesn't already exist + byte[] existing = revokingDB.getUnchecked(rootKey); + if (ArrayUtils.isEmpty(existing)) { + revokingDB.put(rootKey, entry.getValue()); + } + keysToDelete.add(entry.getKey()); + } + + // Phase 3: delete old Turkish keys + for (byte[] key : keysToDelete) { + revokingDB.delete(key); + } + + // Phase 4: mark migration as done + chainBaseManager.getDynamicPropertiesStore().saveTurkishKeyMigrationDone(1); + + logger.info( + "Complete the Turkish key migration, total time: {} milliseconds," + + " total keys: {}, migrated count: {}", + System.currentTimeMillis() - start, totalKeys, entriesToMigrate.size()); + } +} diff --git a/framework/src/main/java/org/tron/core/services/filter/HttpApiAccessFilter.java b/framework/src/main/java/org/tron/core/services/filter/HttpApiAccessFilter.java index 59b9b15582b..f4994d9af08 100644 --- a/framework/src/main/java/org/tron/core/services/filter/HttpApiAccessFilter.java +++ b/framework/src/main/java/org/tron/core/services/filter/HttpApiAccessFilter.java @@ -3,6 +3,7 @@ import com.alibaba.fastjson.JSONObject; import java.net.URI; import java.util.List; +import java.util.Locale; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -63,7 +64,7 @@ private boolean isDisabled(String endpoint) { endpoint = URI.create(endpoint).normalize().toString(); List disabledApiList = CommonParameter.getInstance().getDisabledApiList(); if (!disabledApiList.isEmpty()) { - disabled = disabledApiList.contains(endpoint.split("/")[2].toLowerCase()); + disabled = disabledApiList.contains(endpoint.split("/")[2].toLowerCase(Locale.ROOT)); } } catch (Exception e) { logger.warn("check isDisabled except, endpoint={}, {}", endpoint, e.getMessage()); diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 2b6b929d8a0..f7fda3ac4a0 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -22,6 +22,7 @@ import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import javax.servlet.http.HttpServletRequest; @@ -525,10 +526,10 @@ public static byte[] getAddress(HttpServletRequest request) throws Exception { private static String checkGetParam(HttpServletRequest request, String key) throws Exception { String method = request.getMethod(); - if (HttpMethod.GET.toString().toUpperCase().equalsIgnoreCase(method)) { + if (HttpMethod.GET.toString().toUpperCase(Locale.ROOT).equalsIgnoreCase(method)) { return request.getParameter(key); } - if (HttpMethod.POST.toString().toUpperCase().equals(method)) { + if (HttpMethod.POST.toString().toUpperCase(Locale.ROOT).equals(method)) { String contentType = request.getContentType(); if (StringUtils.isBlank(contentType)) { return null; diff --git a/framework/src/main/java/org/tron/core/services/ratelimiter/RpcApiAccessInterceptor.java b/framework/src/main/java/org/tron/core/services/ratelimiter/RpcApiAccessInterceptor.java index c3471c2829c..d3a6bf74efd 100644 --- a/framework/src/main/java/org/tron/core/services/ratelimiter/RpcApiAccessInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/ratelimiter/RpcApiAccessInterceptor.java @@ -7,6 +7,7 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import java.util.List; +import java.util.Locale; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; @@ -43,7 +44,7 @@ private boolean isDisabled(String endpoint) { try { List disabledApiList = CommonParameter.getInstance().getDisabledApiList(); if (!disabledApiList.isEmpty()) { - disabled = disabledApiList.contains(endpoint.split("/")[1].toLowerCase()); + disabled = disabledApiList.contains(endpoint.split("/")[1].toLowerCase(Locale.ROOT)); } } catch (Exception e) { logger.error("check isDisabled except, endpoint={}, error is {}", endpoint, e.getMessage()); diff --git a/framework/src/main/java/org/tron/keystore/WalletUtils.java b/framework/src/main/java/org/tron/keystore/WalletUtils.java index 8bcc68cbab0..42b33d626e6 100644 --- a/framework/src/main/java/org/tron/keystore/WalletUtils.java +++ b/framework/src/main/java/org/tron/keystore/WalletUtils.java @@ -12,6 +12,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Locale; import java.util.Scanner; import org.apache.commons.lang3.StringUtils; import org.tron.common.crypto.SignInterface; @@ -94,7 +95,7 @@ public static String getDefaultKeyDirectory() { } static String getDefaultKeyDirectory(String osName1) { - String osName = osName1.toLowerCase(); + String osName = osName1.toLowerCase(Locale.ROOT); if (osName.startsWith("mac")) { return String.format( diff --git a/framework/src/main/java/org/tron/program/KeystoreFactory.java b/framework/src/main/java/org/tron/program/KeystoreFactory.java index 8199d7e9076..ea941e54007 100755 --- a/framework/src/main/java/org/tron/program/KeystoreFactory.java +++ b/framework/src/main/java/org/tron/program/KeystoreFactory.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.Locale; import java.util.Scanner; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -114,7 +115,7 @@ private void run() { if ("".equals(cmd)) { continue; } - String cmdLowerCase = cmd.toLowerCase(); + String cmdLowerCase = cmd.toLowerCase(Locale.ROOT); switch (cmdLowerCase) { case "help": { diff --git a/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java b/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java index 5e41670763c..37383690478 100644 --- a/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java +++ b/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java @@ -161,6 +161,7 @@ public void testLastDayOffset() throws Exception { } + @SuppressWarnings("StringCaseLocaleUsage") @Test public void testQuartz() throws Exception { CronExpression cronExpression = new CronExpression("19 15 10 4 Apr ? "); diff --git a/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java b/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java index 87e4e14698c..4fd0fed7f0b 100644 --- a/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java +++ b/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java @@ -116,6 +116,7 @@ public void testInvalidSignatureLength() throws SignatureException { fail("Expecting a SignatureException for invalid signature length"); } + @SuppressWarnings("StringCaseLocaleUsage") @Test public void testSM3Hash() { SM2 key = SM2.fromPublicOnly(pubKey); diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java b/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java index 48355f137f4..92d0da0c4d7 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java @@ -12,6 +12,7 @@ public byte[] compile(String code) { return compile(code.split("\\s+")); } + @SuppressWarnings("StringCaseLocaleUsage") private byte[] compile(String[] tokens) { List bytecodes = new ArrayList<>(); int ntokens = tokens.length; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java index 583b0131942..9a34f0a1434 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java @@ -489,6 +489,7 @@ public void testBlockInformationOperations() throws ContractValidateException { } // test Memory, Storage and Flow Operations + @SuppressWarnings("StringCaseLocaleUsage") @Test public void testMemoryStorageAndFlowOperations() throws ContractValidateException { invoke = new ProgramInvokeMockImpl(); @@ -818,6 +819,7 @@ public void testOtherOperations() throws ContractValidateException { Assert.assertTrue(program.getResult().isRevert()); } + @SuppressWarnings("StringCaseLocaleUsage") @Ignore @Test public void testComplexOperations() throws ContractValidateException { diff --git a/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java b/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java index f676ef6c8e4..56b765301ac 100644 --- a/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java +++ b/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java @@ -430,6 +430,7 @@ public String toPrefixString() { return Hex.toHexString(pref).substring(0, 6); } + @SuppressWarnings("StringCaseLocaleUsage") public String shortHex() { String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase(); return "0x" + hexValue.replaceFirst("^0+(?!$)", ""); diff --git a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java index 236c3464697..cd6244cfb01 100644 --- a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java @@ -1,6 +1,9 @@ package org.tron.core.db; import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Locale; import java.util.Random; import javax.annotation.Resource; import org.junit.Assert; @@ -11,10 +14,35 @@ import org.tron.common.TestConstants; import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BytesCapsule; import org.tron.core.config.args.Args; +import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.store.AccountIdIndexStore; import org.tron.protos.Protocol.AccountType; +/** + * Tests for {@link AccountIdIndexStore}, including the Turkish-I locale fix. + * + *

Background: {@code String.toLowerCase()} without an explicit + * Locale uses {@code Locale.getDefault()}. On Turkish (tr) / Azerbaijani (az) locales, + * uppercase 'I' (U+0049) folds to dotless 'ı' (U+0131) instead of 'i' (U+0069). + * This caused different index keys on tr/az vs other nodes → consensus split.

+ * + *

Test scenario coverage

+ *
+ *  #1  getLowerCaseAccountId uses Locale.ROOT
+ *  #2  Turkish locale produces different key
+ *  #3  Normal put/get with random bytes
+ *  #4  Normal put/has with random bytes
+ *  #5  Case-insensitive: mixed/lower/upper lookup
+ *  #6  Attack: uppercase put + lowercase has
+ *  #7  No 'I': ROOT and Turkish keys identical
+ *  #8  Turkish legacy key: uppercase input
+ *  #9  Turkish legacy key: lowercase input
+ *  #10 Turkish legacy key: mixed-case input
+ *  #11 Locale migration: tr/az write, non-tr/az read
+ * 
+ */ public class AccountIdIndexStoreTest extends BaseTest { private static final byte[] ACCOUNT_ADDRESS_ONE = randomBytes(16); @@ -26,6 +54,7 @@ public class AccountIdIndexStoreTest extends BaseTest { private static final byte[] ACCOUNT_NAME_THREE = randomBytes(6); private static final byte[] ACCOUNT_NAME_FOUR = randomBytes(6); private static final byte[] ACCOUNT_NAME_FIVE = randomBytes(6); + private static final Locale TURKISH = Locale.forLanguageTag("tr"); @Resource private AccountIdIndexStore accountIdIndexStore; private static AccountCapsule accountCapsule1; @@ -71,24 +100,26 @@ public static byte[] randomBytes(int length) { return result; } + /** Scenario #3: normal put/get with random bytes. */ @Test public void putAndGet() { byte[] address = accountIdIndexStore.get(ByteString.copyFrom(ACCOUNT_NAME_ONE)); - Assert.assertArrayEquals("putAndGet1", address, ACCOUNT_ADDRESS_ONE); + Assert.assertArrayEquals("putAndGet1", ACCOUNT_ADDRESS_ONE, address); address = accountIdIndexStore.get(ByteString.copyFrom(ACCOUNT_NAME_TWO)); - Assert.assertArrayEquals("putAndGet2", address, ACCOUNT_ADDRESS_TWO); + Assert.assertArrayEquals("putAndGet2", ACCOUNT_ADDRESS_TWO, address); address = accountIdIndexStore.get(ByteString.copyFrom(ACCOUNT_NAME_THREE)); - Assert.assertArrayEquals("putAndGet3", address, ACCOUNT_ADDRESS_THREE); + Assert.assertArrayEquals("putAndGet3", ACCOUNT_ADDRESS_THREE, address); address = accountIdIndexStore.get(ByteString.copyFrom(ACCOUNT_NAME_FOUR)); - Assert.assertArrayEquals("putAndGet4", address, ACCOUNT_ADDRESS_FOUR); + Assert.assertArrayEquals("putAndGet4", ACCOUNT_ADDRESS_FOUR, address); address = accountIdIndexStore.get(ByteString.copyFrom(ACCOUNT_NAME_FIVE)); Assert.assertNull("putAndGet4", address); } + /** Scenario #4: normal put/has with random bytes. */ @Test public void putAndHas() { - Boolean result = accountIdIndexStore.has(ACCOUNT_NAME_ONE); + boolean result = accountIdIndexStore.has(ACCOUNT_NAME_ONE); Assert.assertTrue("putAndGet1", result); result = accountIdIndexStore.has(ACCOUNT_NAME_TWO); Assert.assertTrue("putAndGet2", result); @@ -100,6 +131,7 @@ public void putAndHas() { Assert.assertFalse("putAndGet4", result); } + /** Scenario #5: case-insensitive lookup with mixed/lower/upper. */ @Test public void testCaseInsensitive() { byte[] ACCOUNT_NAME = "aABbCcDd_ssd1234".getBytes(); @@ -114,13 +146,13 @@ public void testCaseInsensitive() { Assert.assertTrue("fail", result); byte[] lowerCase = ByteString - .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toLowerCase()) + .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toLowerCase(Locale.ROOT)) .toByteArray(); result = accountIdIndexStore.has(lowerCase); Assert.assertTrue("lowerCase fail", result); byte[] upperCase = ByteString - .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toUpperCase()) + .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toUpperCase(Locale.ROOT)) .toByteArray(); result = accountIdIndexStore.has(upperCase); Assert.assertTrue("upperCase fail", result); @@ -128,4 +160,373 @@ public void testCaseInsensitive() { Assert.assertNotNull("getLowerCase fail", accountIdIndexStore.get(upperCase)); } -} \ No newline at end of file + + /** + * Scenario #1 and #2: getLowerCaseAccountId uses Locale.ROOT, and Turkish + * locale would produce a different key for input containing 'I'. + */ + @Test + public void testLocaleIndependentLowerCase() { + byte[] accountId = "AAAAAAAI".getBytes(StandardCharsets.UTF_8); + byte[] expected = "aaaaaaai".getBytes(StandardCharsets.UTF_8); + + // #1: getLowerCaseAccountId must always produce standard ASCII lowercase + byte[] actual = AccountIdIndexStore.getLowerCaseAccountId(accountId); + Assert.assertArrayEquals( + "getLowerCaseAccountId must use Locale.ROOT to avoid Turkish-I problem", + expected, actual); + + // #2: Turkish locale produces a different key (dotless-ı) + @SuppressWarnings("StringCaseLocaleUsage") + String turkishLower = new String(accountId, StandardCharsets.UTF_8) + .toLowerCase(TURKISH); + byte[] turkishKey = turkishLower.getBytes(StandardCharsets.UTF_8); + Assert.assertFalse( + "Turkish locale toLowerCase must differ from Locale.ROOT for input containing 'I'", + Arrays.equals(expected, turkishKey)); + } + + /** + * Scenario #6: consensus-split attack — uppercase put, lowercase has/get. + * + *
+   * 1. T1(accountId="AAAAAAAI") put → stored key "aaaaaaai"
+   * 2. Attacker submits T2(accountId="aaaaaaai")
+   * 3. has("aaaaaaai") must return true → reject T2
+   * 
+ */ + @Test + public void testDuplicateAccountIdDetection() { + byte[] upperCaseId = "AAAAAAAI".getBytes(StandardCharsets.UTF_8); + byte[] lowerCaseId = "aaaaaaai".getBytes(StandardCharsets.UTF_8); + byte[] address = randomBytes(16); + + AccountCapsule capsule = new AccountCapsule( + ByteString.copyFrom(address), + ByteString.copyFrom(upperCaseId), AccountType.Normal); + capsule.setAccountId(ByteString.copyFrom(upperCaseId).toByteArray()); + + accountIdIndexStore.put(capsule); + + Assert.assertTrue( + "has() must detect duplicate accountId regardless of case", + accountIdIndexStore.has(lowerCaseId)); + Assert.assertTrue( + "has() must detect duplicate accountId with original case", + accountIdIndexStore.has(upperCaseId)); + + Assert.assertNotNull( + "get() must find accountId regardless of case", + accountIdIndexStore.get(lowerCaseId)); + Assert.assertArrayEquals( + "get() must return correct address for case-insensitive lookup", + address, accountIdIndexStore.get(ByteString.copyFrom(lowerCaseId))); + } + + /** + * Scenario #7: input without 'I' — ROOT and Turkish keys must be identical. + */ + @Test + public void testNoTurkishICharacter() { + byte[] accountId = "ABCDEFGH".getBytes(StandardCharsets.UTF_8); + byte[] rootKey = AccountIdIndexStore.getLowerCaseAccountId(accountId); + + // Turkish toLowerCase of a string without 'I' should equal ROOT toLowerCase + @SuppressWarnings("StringCaseLocaleUsage") + byte[] turkishKey = ByteString + .copyFromUtf8(new String(accountId, StandardCharsets.UTF_8).toLowerCase(TURKISH)) + .toByteArray(); + + Assert.assertArrayEquals( + "Without 'I', ROOT and Turkish keys must be identical", + rootKey, turkishKey); + } + + /** + * Verify which characters in the valid accountId range (0x21 '!' to 0x7E '~') + * differ between ROOT and Turkish toLowerCase. + * Expected: only 'I' (U+0049) differs. + * + * @see org.tron.core.utils.TransactionUtil#validReadableBytes + */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testTurkishLowerCaseDiffForValidAccountIdRange() { + StringBuilder diffChars = new StringBuilder(); + // 0x21 ('!') to 0x7E ('~') — the full valid accountId byte range + for (char c = 0x21; c <= 0x7E; c++) { + String s = String.valueOf(c); + String rootLower = s.toLowerCase(Locale.ROOT); + String turkishLower = s.toLowerCase(TURKISH); + if (!rootLower.equals(turkishLower)) { + diffChars.append(String.format( + "'%c'(0x%02X): ROOT='%s'(U+%04X) vs TR='%s'(U+%04X); ", + c, (int) c, + rootLower, (int) rootLower.charAt(0), + turkishLower, (int) turkishLower.charAt(0))); + } + } + + String diff = diffChars.toString().trim(); + // Expect exactly one diff entry: 'I' + Assert.assertEquals( + "Only 'I' should differ in valid accountId range. Actual: " + diff, + "'I'(0x49): ROOT='i'(U+0069) vs TR='" + + "\u0131'(U+0131);", diff); // ı Turkish dotless-i + } + + /** + * Scenario #8: direct Turkish key — toLowerCase(TURKISH) reproduces + * the exact key a Turkish node stored for the same-case input. + * + *
+   * +-------+----------------+---------------------+
+   * | Case  | Input          | toLower(TURKISH)    |
+   * +-------+----------------+---------------------+
+   * |  #8a  | "AAAAAAAI"     | "aaaaaaaı"          |
+   * |  #8b  | "aaaaaaai"     | "aaaaaaai"          |
+   * |  #8c  | "AaAaAaAI"     | "aaaaaaaı"          |
+   * |  #8d  | "AiBI"         | "aibı"              |
+   * +-------+----------------+---------------------+
+   * 
+ */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testTurkishDirectKey() { + // #8a: all uppercase with 'I' → 'ı' + Assert.assertEquals("aaaaaaaı", + "AAAAAAAI".toLowerCase(TURKISH)); + // #8b: all lowercase — 'i' stays 'i' + Assert.assertEquals("aaaaaaai", + "aaaaaaai".toLowerCase(TURKISH)); + // #8c: mixed case with uppercase 'I' + Assert.assertEquals("aaaaaaaı", + "AaAaAaAI".toLowerCase(TURKISH)); + // #8d: mixed i/I — each mapped independently + Assert.assertEquals("aibı", + "AiBI".toLowerCase(TURKISH)); + } + + /** + * Scenario #9: normalized Turkish key — ROOT key with all 'i' replaced + * by 'ı'. This handles cross-case queries. + * + *
+   * +-------+----------------+-----------+---------------------+
+   * | Case  | Input          | ROOT key  | normalized key      |
+   * +-------+----------------+-----------+---------------------+
+   * |  #9a  | "AAAAAAAI"     | "aaaaaaai"| "aaaaaaaı"          |
+   * |  #9b  | "aaaaaaai"     | "aaaaaaai"| "aaaaaaaı"          |
+   * |  #9c  | "ABCDEFGH"     | "abcdefgh"| "abcdefgh" (same)   |
+   * +-------+----------------+-----------+---------------------+
+   * 
+ */ + @Test + public void testTurkishNormalizedKey() { + // #9a/#9b: any case variant with 'i' → all 'i' become 'ı' + byte[] rootKey = AccountIdIndexStore.getLowerCaseAccountId( + "AAAAAAAI".getBytes(StandardCharsets.UTF_8)); + String normalized = new String(rootKey, StandardCharsets.UTF_8) + .replace('i', '\u0131'); // ı Turkish dotless-i + Assert.assertEquals("aaaaaaaı", normalized); + + // same result for lowercase input + byte[] rootKey2 = AccountIdIndexStore.getLowerCaseAccountId( + "aaaaaaai".getBytes(StandardCharsets.UTF_8)); + Assert.assertEquals(normalized, + new String(rootKey2, StandardCharsets.UTF_8) + .replace('i', '\u0131')); // ı Turkish dotless-i + + // #9c: no 'i' → normalized equals ROOT (no extra probe needed) + byte[] rootKeyNoI = AccountIdIndexStore.getLowerCaseAccountId( + "ABCDEFGH".getBytes(StandardCharsets.UTF_8)); + String normalizedNoI = new String(rootKeyNoI, StandardCharsets.UTF_8) + .replace('i', '\u0131'); // ı Turkish dotless-i + Assert.assertEquals("abcdefgh", normalizedNoI); + } + + /** + * Scenario #10: locale migration — all uppercase 'I'. + * Turkish node stored "BBBBBBBI" → key "bbbbbbbbı". + * All query case variants must find it. + * + *
+   * stored: "BBBBBBBI".toLower(TR) = "bbbbbbbı"
+   * +---+-----------+-------+--------+------------+--------+
+   * |   | query     | ROOT  | direct | normalized | result |
+   * +---+-----------+-------+--------+------------+--------+
+   * | a | "BBBBBBBI"| miss  | hit    | -          |  ✓     |
+   * | b | "bbbbbbbi"| miss  | skip   | hit        |  ✓     |
+   * | c | "BbBbBbBi"| miss  | miss   | hit        |  ✓     |
+   * +---+-----------+-------+--------+------------+--------+
+   * 
+ */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testLocaleMigrationAllUpperI() { + byte[] accountId = "BBBBBBBI".getBytes(StandardCharsets.UTF_8); + byte[] address = randomBytes(16); + + byte[] legacyKey = new String(accountId, StandardCharsets.UTF_8) + .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); + accountIdIndexStore.put(legacyKey, new BytesCapsule(address)); + + // (a) same-case query — found via direct fallback + Assert.assertTrue("has(BBBBBBBI)", accountIdIndexStore.has(accountId)); + Assert.assertArrayEquals("get(BBBBBBBI)", + address, accountIdIndexStore.get(ByteString.copyFrom(accountId))); + + // (b) all-lowercase query — found via normalized fallback + byte[] lowerQuery = "bbbbbbbi".getBytes(StandardCharsets.UTF_8); + Assert.assertTrue("has(bbbbbbbi)", accountIdIndexStore.has(lowerQuery)); + Assert.assertArrayEquals("get(bbbbbbbi)", + address, accountIdIndexStore.get(ByteString.copyFrom(lowerQuery))); + + // (c) mixed-case query (lowercase 'i') — found via normalized fallback + byte[] mixedQuery = "BbBbBbBi".getBytes(StandardCharsets.UTF_8); + Assert.assertTrue("has(BbBbBbBi)", accountIdIndexStore.has(mixedQuery)); + Assert.assertArrayEquals("get(BbBbBbBi)", + address, accountIdIndexStore.get(ByteString.copyFrom(mixedQuery))); + } + + /** + * Scenario #11: locale migration — two uppercase 'I' (like "AIBI"). + * All query case variants must find it. + * + *
+   * stored: "DDIDDI".toLower(TR) = "ddıddı"
+   * +---+-----------+-------+--------+------------+--------+
+   * |   | query     | ROOT  | direct | normalized | result |
+   * +---+-----------+-------+--------+------------+--------+
+   * | a | "DDIDDI"  | miss  | hit    | -          |  ✓     |
+   * | b | "ddiddi"  | miss  | skip   | hit        |  ✓     |
+   * | c | "DdIddI"  | miss  | hit    | -          |  ✓     |
+   * | d | "DdiDdi"  | miss  | miss   | hit        |  ✓     |
+   * +---+-----------+-------+--------+------------+--------+
+   * 
+ */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testLocaleMigrationMultipleUpperI() { + byte[] accountId = "DDIDDI".getBytes(StandardCharsets.UTF_8); + byte[] address = randomBytes(16); + + byte[] legacyKey = new String(accountId, StandardCharsets.UTF_8) + .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); + accountIdIndexStore.put(legacyKey, new BytesCapsule(address)); + + // (a) same-case + Assert.assertArrayEquals("get(DDIDDI)", + address, accountIdIndexStore.get(ByteString.copyFrom(accountId))); + // (b) all-lowercase — normalized fallback + Assert.assertArrayEquals("get(ddiddi)", address, + accountIdIndexStore.get(ByteString.copyFrom( + "ddiddi".getBytes(StandardCharsets.UTF_8)))); + // (c) same uppercase I positions — direct fallback + Assert.assertArrayEquals("get(DdIddI)", address, + accountIdIndexStore.get(ByteString.copyFrom( + "DdIddI".getBytes(StandardCharsets.UTF_8)))); + // (d) all lowercase i — normalized fallback + Assert.assertArrayEquals("get(DdiDdi)", address, + accountIdIndexStore.get(ByteString.copyFrom( + "DdiDdi".getBytes(StandardCharsets.UTF_8)))); + } + + /** + * Scenario #12: Turkish key migration — mixed 'i' and 'I' (like "AIBi", "AiBI"). + * Before migration, cross-case query was a known limitation. + * After migrateTurkishKeys(), all Turkish keys are normalized to ROOT, + * so both same-case and cross-case queries work. + * + *
+   * stored(1): "EEIEEi".toLower(TR) = "eeıeei" → migrated to "eeieei"
+   * stored(2): "FFiFFI".toLower(TR) = "ffiffı" → migrated to "ffiffi"
+   * 
+ */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testMigrateTurkishKeysMixedCase() { + // --- pattern 1: uppercase I before lowercase i ("EEIEEi") --- + byte[] accountId1 = "EEIEEi".getBytes(StandardCharsets.UTF_8); + byte[] address1 = randomBytes(16); + byte[] legacyKey1 = new String(accountId1, StandardCharsets.UTF_8) + .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); + accountIdIndexStore.put(legacyKey1, new BytesCapsule(address1)); + + // --- pattern 2: lowercase i before uppercase I ("FFiFFI") --- + byte[] accountId2 = "FFiFFI".getBytes(StandardCharsets.UTF_8); + byte[] address2 = randomBytes(16); + byte[] legacyKey2 = new String(accountId2, StandardCharsets.UTF_8) + .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); + accountIdIndexStore.put(legacyKey2, new BytesCapsule(address2)); + + // Before migration: cross-case query misses (mixed i/ı key) + Assert.assertFalse("pre-migrate: has(eeieei) should miss", + accountIdIndexStore.has("eeieei".getBytes(StandardCharsets.UTF_8))); + Assert.assertFalse("pre-migrate: has(ffiffi) should miss", + accountIdIndexStore.has("ffiffi".getBytes(StandardCharsets.UTF_8))); + + // Run migration via the standard helper + new MigrateTurkishKeyHelper(chainBaseManager).doWork(); + + // After migration: all queries work via ROOT key + Assert.assertTrue("post-migrate: has(EEIEEi)", + accountIdIndexStore.has(accountId1)); + Assert.assertTrue("post-migrate: has(eeieei)", + accountIdIndexStore.has("eeieei".getBytes(StandardCharsets.UTF_8))); + Assert.assertArrayEquals("post-migrate: get(eeieei)", + address1, accountIdIndexStore.get(ByteString.copyFrom( + "eeieei".getBytes(StandardCharsets.UTF_8)))); + + Assert.assertTrue("post-migrate: has(FFiFFI)", + accountIdIndexStore.has(accountId2)); + Assert.assertTrue("post-migrate: has(ffiffi)", + accountIdIndexStore.has("ffiffi".getBytes(StandardCharsets.UTF_8))); + Assert.assertArrayEquals("post-migrate: get(ffiffi)", + address2, accountIdIndexStore.get(ByteString.copyFrom( + "ffiffi".getBytes(StandardCharsets.UTF_8)))); + + // Verify migration wrote ROOT keys (replace 'ı' → 'i') + byte[] rootKey1 = new String(legacyKey1, StandardCharsets.UTF_8) + .replace('\u0131', 'i') // ı Turkish dotless-i + .getBytes(StandardCharsets.UTF_8); + byte[] rootKey2 = new String(legacyKey2, StandardCharsets.UTF_8) + .replace('\u0131', 'i') // ı Turkish dotless-i + .getBytes(StandardCharsets.UTF_8); + // ROOT keys must exist and return correct addresses + Assert.assertArrayEquals("post-migrate: ROOT key1 must exist", + address1, accountIdIndexStore.get(ByteString.copyFrom(rootKey1))); + Assert.assertArrayEquals("post-migrate: ROOT key2 must exist", + address2, accountIdIndexStore.get(ByteString.copyFrom(rootKey2))); + } + + /** + * Scenario #13: accountId with only lowercase 'i' (like "Ai", "AiBi"). + * Turkish node stored the same key as ROOT — no fallback needed. + */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testLocaleMigrationOnlyLowerI() { + byte[] accountId = "GGiGGi".getBytes(StandardCharsets.UTF_8); + byte[] address = randomBytes(16); + + // Turkish key = ROOT key (lowercase 'i' is unaffected by Turkish locale) + byte[] legacyKey = new String(accountId, StandardCharsets.UTF_8) + .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); + byte[] rootKey = AccountIdIndexStore.getLowerCaseAccountId(accountId); + Assert.assertArrayEquals("Only lowercase 'i': Turkish key must equal ROOT key", + rootKey, legacyKey); + + accountIdIndexStore.put(legacyKey, new BytesCapsule(address)); + + // Any case variant works via ROOT key + Assert.assertArrayEquals("get(same-case)", address, + accountIdIndexStore.get(ByteString.copyFrom(accountId))); + Assert.assertArrayEquals("get(lowercase)", address, + accountIdIndexStore.get(ByteString.copyFrom( + "ggiggi".getBytes(StandardCharsets.UTF_8)))); + Assert.assertArrayEquals("get(uppercase)", address, + accountIdIndexStore.get(ByteString.copyFrom( + "GGIGGI".getBytes(StandardCharsets.UTF_8)))); + } +} diff --git a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java index 7150f1a0541..562af84cc4c 100755 --- a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java @@ -2527,6 +2527,7 @@ public void pushSameSkAndScanAndSpend() throws Exception { Assert.assertTrue(ok2); } + @SuppressWarnings("StringCaseLocaleUsage") @Test public void decodePaymentAddressIgnoreCase() { String addressLower = diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4d0bf1013d6..487f43aaf06 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -247,6 +247,20 @@ + + + + + + + + + + + + + + @@ -266,6 +280,14 @@ + + + + + + + + @@ -300,6 +322,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -350,6 +435,14 @@ + + + + + + + + @@ -359,6 +452,9 @@ + + + @@ -371,6 +467,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -386,6 +506,24 @@ + + + + + + + + + + + + + + + + + + @@ -394,6 +532,14 @@ + + + + + + + + @@ -455,6 +601,17 @@ + + + + + + + + + + + @@ -495,6 +652,11 @@ + + + + + @@ -512,6 +674,9 @@ + + + @@ -524,6 +689,11 @@ + + + + + @@ -537,6 +707,14 @@ + + + + + + + + @@ -553,6 +731,11 @@ + + + + + @@ -587,6 +770,9 @@ + + + @@ -817,6 +1003,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -1288,6 +1495,22 @@ + + + + + + + + + + + + + + + + @@ -1652,6 +1875,25 @@ + + + + + + + + + + + + + + + + + + + @@ -1863,6 +2105,9 @@ + + + @@ -1989,6 +2234,17 @@ + + + + + + + + + + + @@ -2145,6 +2401,17 @@ + + + + + + + + + + + diff --git a/platform/src/main/java/common/org/tron/common/arch/Arch.java b/platform/src/main/java/common/org/tron/common/arch/Arch.java index f115d1f07c2..999bb631bea 100644 --- a/platform/src/main/java/common/org/tron/common/arch/Arch.java +++ b/platform/src/main/java/common/org/tron/common/arch/Arch.java @@ -1,5 +1,6 @@ package org.tron.common.arch; +import java.util.Locale; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "arch") @@ -21,11 +22,11 @@ public static String withAll() { } public static String getOsName() { - return System.getProperty("os.name").toLowerCase().trim(); + return System.getProperty("os.name").toLowerCase(Locale.ROOT).trim(); } public static String getOsArch() { - return System.getProperty("os.arch").toLowerCase().trim(); + return System.getProperty("os.arch").toLowerCase(Locale.ROOT).trim(); } public static int getBitModel() { @@ -45,15 +46,15 @@ public static int getBitModel() { } public static String javaVersion() { - return System.getProperty("java.version").toLowerCase().trim(); + return System.getProperty("java.version").toLowerCase(Locale.ROOT).trim(); } public static String javaSpecificationVersion() { - return System.getProperty("java.specification.version").toLowerCase().trim(); + return System.getProperty("java.specification.version").toLowerCase(Locale.ROOT).trim(); } public static String javaVendor() { - return System.getProperty("java.vendor").toLowerCase().trim(); + return System.getProperty("java.vendor").toLowerCase(Locale.ROOT).trim(); } public static boolean isArm64() { diff --git a/settings.gradle b/settings.gradle index af32bfca702..0a1fd84bdf9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,9 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} rootProject.name = 'java-tron' include 'framework' include 'chainbase' @@ -9,4 +15,5 @@ include 'example:actuator-example' include 'crypto' include 'plugins' include 'platform' +include 'errorprone' From 897d4e8cdb99d0d3f5069a47740219a16bac27ec Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Mon, 13 Apr 2026 16:07:57 +0800 Subject: [PATCH 2/4] refactor: remove Turkish fallback lookup, rely solely on migration The dual Turkish fallback (direct + normalized probes) in AccountIdIndexStore is no longer needed because: 1. MigrateTurkishKeyHelper normalizes all legacy keys at startup before any queries are served 2. New writes always use Locale.ROOT, so no new Turkish keys are created 3. If migration is interrupted, the DynamicPropertiesStore flag ensures it reruns on next startup This simplifies get()/has() to single-lookup operations and removes ~100 lines of fallback logic (DOTLESS_I, TURKISH locale, direct/ normalized key methods, lookupWithFallback). Co-Authored-By: Claude Opus 4.6 --- .../tron/core/store/AccountIdIndexStore.java | 115 ++-------- .../tron/core/db/AccountIdIndexStoreTest.java | 216 ++++-------------- 2 files changed, 61 insertions(+), 270 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java b/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java index 9aad6992d27..529af5bee0a 100644 --- a/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java +++ b/chainbase/src/main/java/org/tron/core/store/AccountIdIndexStore.java @@ -1,11 +1,8 @@ package org.tron.core.store; import com.google.protobuf.ByteString; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.Locale; import java.util.Objects; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -14,129 +11,49 @@ import org.tron.core.capsule.BytesCapsule; import org.tron.core.db.TronStoreWithRevoking; -@Slf4j(topic = "DB") +//todo : need Compatibility test @Component public class AccountIdIndexStore extends TronStoreWithRevoking { - /** - * Turkish dotless-ı (U+0131). On Turkish/Azerbaijani locales, - * {@code 'I'.toLowerCase()} produces this instead of ASCII {@code 'i'}. - * This is the ONLY ASCII letter that differs between ROOT and Turkish - * {@code toLowerCase()} — verified by testTurkishLowerCaseDiffForAllAsciiLetters. - */ - private static final char DOTLESS_I = '\u0131'; // ı Turkish dotless-i - private static final Locale TURKISH = Locale.forLanguageTag("tr"); - @Autowired public AccountIdIndexStore(@Value("accountid-index") String dbName) { super(dbName); } - public static byte[] getLowerCaseAccountId(byte[] accountId) { + public static byte[] getLowerCaseAccountId(byte[] bsAccountId) { return ByteString - .copyFromUtf8(ByteString.copyFrom(accountId).toStringUtf8().toLowerCase(Locale.ROOT)) + .copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(Locale.ROOT)) .toByteArray(); } - /** - * Turkish direct key: {@code toLowerCase(TURKISH)} on the original input. - * Reproduces the exact key a Turkish node stored for the same-case input. - * Handles lookups where query case matches the original accountId case. - * - *

Example: input "AiBI" → "aibı" (lowercase 'i' stays, uppercase 'I' → 'ı'). - */ - @SuppressWarnings("StringCaseLocaleUsage") - private static byte[] getTurkishDirectKey(byte[] accountId) { - String str = ByteString.copyFrom(accountId).toStringUtf8(); - return ByteString.copyFromUtf8(str.toLowerCase(TURKISH)).toByteArray(); - } - - /** - * Turkish normalized key: ROOT key with all {@code 'i'} replaced by {@code 'ı'}. - * Handles cross-case lookups (e.g., lowercase query for an accountId that - * was originally uppercase on a Turkish node). - * - *

Example: rootKey "aibi" → "aıbı". - * - * @param rootKey the already-computed ROOT-lowered key - * @return the normalized key, or {@code rootKey} itself if no 'i' is present - */ - private static byte[] getTurkishNormalizedKey(byte[] rootKey) { - String str = new String(rootKey, StandardCharsets.UTF_8); - if (str.indexOf('i') < 0) { - return rootKey; - } - return str.replace('i', DOTLESS_I).getBytes(StandardCharsets.UTF_8); - } - public void put(AccountCapsule accountCapsule) { byte[] lowerCaseAccountId = getLowerCaseAccountId(accountCapsule.getAccountId().toByteArray()); super.put(lowerCaseAccountId, new BytesCapsule(accountCapsule.getAddress().toByteArray())); } - public byte[] get(ByteString accountId) { - BytesCapsule bytesCapsule = get(accountId.toByteArray()); + public byte[] get(ByteString name) { + BytesCapsule bytesCapsule = get(name.toByteArray()); if (Objects.nonNull(bytesCapsule)) { return bytesCapsule.getData(); } return null; } - /** - * Look up by the standard (Locale.ROOT) accountId first; on miss, fall back to - * Turkish legacy keys. The fallback covers nodes that previously ran under - * tr/az locale and wrote keys containing dotless-ı (U+0131). - * - *

Two fallback probes are used: - *

    - *
  1. Direct: {@code toLowerCase(TURKISH)} — matches when query - * case equals original accountId case (handles mixed 'i'/'I').
  2. - *
  3. Normalized: ROOT accountId with all 'i' → 'ı' — matches when - * query case differs from original (e.g., all-lowercase query for - * an all-uppercase stored accountId).
  4. - *
- * - *

Each probe is skipped when it produces the same accountId as the ROOT accountId - * (i.e., input contains no 'I' or 'i'). AccountIdIndexStore is a small - * dataset, so the overhead of up to two extra lookups is negligible. - */ @Override - public BytesCapsule get(byte[] accountId) { - byte[] value = lookupWithFallback(accountId); - return ArrayUtils.isEmpty(value) ? null : new BytesCapsule(value); + public BytesCapsule get(byte[] key) { + byte[] lowerCaseKey = getLowerCaseAccountId(key); + byte[] value = revokingDB.getUnchecked(lowerCaseKey); + if (ArrayUtils.isEmpty(value)) { + return null; + } + return new BytesCapsule(value); } - /** See {@link #get(byte[])} for fallback strategy. */ @Override - public boolean has(byte[] accountId) { - return !ArrayUtils.isEmpty(lookupWithFallback(accountId)); - } - - private byte[] lookupWithFallback(byte[] accountId) { - byte[] rootLocaleKey = getLowerCaseAccountId(accountId); - byte[] value = revokingDB.getUnchecked(rootLocaleKey); - // Fallback 1: direct Turkish accountId (same-case match). - // Needed for accountIds containing BOTH 'i' and 'I' (e.g., "AiBI"). - // A Turkish node stored toLowerCase(TURKISH) = "aibı" — only the - // direct probe reproduces this mixed 'i'/'ı' key correctly. - // The normalized probe (Fallback 2) would produce "aıbı" instead. - if (ArrayUtils.isEmpty(value)) { - byte[] directKey = getTurkishDirectKey(accountId); - if (!Arrays.equals(rootLocaleKey, directKey)) { - value = revokingDB.getUnchecked(directKey); - } - } - // Fallback 2: normalized Turkish accountId (cross-case match). - // Handles queries where case differs from the original accountId, - // e.g., lowercase "aibi" looking up an entry stored as "AIBI" - // on a Turkish node (stored key = "aıbı"). - if (ArrayUtils.isEmpty(value)) { - byte[] normalizedKey = getTurkishNormalizedKey(rootLocaleKey); - if (!Arrays.equals(rootLocaleKey, normalizedKey)) { - value = revokingDB.getUnchecked(normalizedKey); - } - } - return value; + public boolean has(byte[] key) { + byte[] lowerCaseKey = getLowerCaseAccountId(key); + byte[] value = revokingDB.getUnchecked(lowerCaseKey); + return !ArrayUtils.isEmpty(value); } } diff --git a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java index cd6244cfb01..80323841214 100644 --- a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java +++ b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java @@ -37,10 +37,10 @@ * #5 Case-insensitive: mixed/lower/upper lookup * #6 Attack: uppercase put + lowercase has * #7 No 'I': ROOT and Turkish keys identical - * #8 Turkish legacy key: uppercase input - * #9 Turkish legacy key: lowercase input - * #10 Turkish legacy key: mixed-case input - * #11 Locale migration: tr/az write, non-tr/az read + * #8 Turkish direct key behavior (toLowerCase(TURKISH)) + * #9 Valid accountId range: only 'I' differs + * #10 Migration: mixed 'i'/'I' keys normalized to ROOT + * #11 Only lowercase 'i': Turkish key equals ROOT key * */ public class AccountIdIndexStoreTest extends BaseTest { @@ -134,25 +134,25 @@ public void putAndHas() { /** Scenario #5: case-insensitive lookup with mixed/lower/upper. */ @Test public void testCaseInsensitive() { - byte[] ACCOUNT_NAME = "aABbCcDd_ssd1234".getBytes(); - byte[] ACCOUNT_ADDRESS = randomBytes(16); + byte[] accountName = "aABbCcDd_ssd1234".getBytes(); + byte[] accountAddress = randomBytes(16); - AccountCapsule accountCapsule = new AccountCapsule(ByteString.copyFrom(ACCOUNT_ADDRESS), - ByteString.copyFrom(ACCOUNT_NAME), AccountType.Normal); - accountCapsule.setAccountId(ByteString.copyFrom(ACCOUNT_NAME).toByteArray()); + AccountCapsule accountCapsule = new AccountCapsule(ByteString.copyFrom(accountAddress), + ByteString.copyFrom(accountName), AccountType.Normal); + accountCapsule.setAccountId(ByteString.copyFrom(accountName).toByteArray()); accountIdIndexStore.put(accountCapsule); - Boolean result = accountIdIndexStore.has(ACCOUNT_NAME); + Boolean result = accountIdIndexStore.has(accountName); Assert.assertTrue("fail", result); byte[] lowerCase = ByteString - .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toLowerCase(Locale.ROOT)) + .copyFromUtf8(ByteString.copyFrom(accountName).toStringUtf8().toLowerCase(Locale.ROOT)) .toByteArray(); result = accountIdIndexStore.has(lowerCase); Assert.assertTrue("lowerCase fail", result); byte[] upperCase = ByteString - .copyFromUtf8(ByteString.copyFrom(ACCOUNT_NAME).toStringUtf8().toUpperCase(Locale.ROOT)) + .copyFromUtf8(ByteString.copyFrom(accountName).toStringUtf8().toUpperCase(Locale.ROOT)) .toByteArray(); result = accountIdIndexStore.has(upperCase); Assert.assertTrue("upperCase fail", result); @@ -243,41 +243,8 @@ public void testNoTurkishICharacter() { } /** - * Verify which characters in the valid accountId range (0x21 '!' to 0x7E '~') - * differ between ROOT and Turkish toLowerCase. - * Expected: only 'I' (U+0049) differs. - * - * @see org.tron.core.utils.TransactionUtil#validReadableBytes - */ - @Test - @SuppressWarnings("StringCaseLocaleUsage") - public void testTurkishLowerCaseDiffForValidAccountIdRange() { - StringBuilder diffChars = new StringBuilder(); - // 0x21 ('!') to 0x7E ('~') — the full valid accountId byte range - for (char c = 0x21; c <= 0x7E; c++) { - String s = String.valueOf(c); - String rootLower = s.toLowerCase(Locale.ROOT); - String turkishLower = s.toLowerCase(TURKISH); - if (!rootLower.equals(turkishLower)) { - diffChars.append(String.format( - "'%c'(0x%02X): ROOT='%s'(U+%04X) vs TR='%s'(U+%04X); ", - c, (int) c, - rootLower, (int) rootLower.charAt(0), - turkishLower, (int) turkishLower.charAt(0))); - } - } - - String diff = diffChars.toString().trim(); - // Expect exactly one diff entry: 'I' - Assert.assertEquals( - "Only 'I' should differ in valid accountId range. Actual: " + diff, - "'I'(0x49): ROOT='i'(U+0069) vs TR='" - + "\u0131'(U+0131);", diff); // ı Turkish dotless-i - } - - /** - * Scenario #8: direct Turkish key — toLowerCase(TURKISH) reproduces - * the exact key a Turkish node stored for the same-case input. + * Scenario #8: Turkish direct key — toLowerCase(TURKISH) produces + * different results for inputs containing uppercase 'I'. * *

    * +-------+----------------+---------------------+
@@ -308,135 +275,42 @@ public void testTurkishDirectKey() {
   }
 
   /**
-   * Scenario #9: normalized Turkish key — ROOT key with all 'i' replaced
-   * by 'ı'. This handles cross-case queries.
-   *
-   * 
-   * +-------+----------------+-----------+---------------------+
-   * | Case  | Input          | ROOT key  | normalized key      |
-   * +-------+----------------+-----------+---------------------+
-   * |  #9a  | "AAAAAAAI"     | "aaaaaaai"| "aaaaaaaı"          |
-   * |  #9b  | "aaaaaaai"     | "aaaaaaai"| "aaaaaaaı"          |
-   * |  #9c  | "ABCDEFGH"     | "abcdefgh"| "abcdefgh" (same)   |
-   * +-------+----------------+-----------+---------------------+
-   * 
- */ - @Test - public void testTurkishNormalizedKey() { - // #9a/#9b: any case variant with 'i' → all 'i' become 'ı' - byte[] rootKey = AccountIdIndexStore.getLowerCaseAccountId( - "AAAAAAAI".getBytes(StandardCharsets.UTF_8)); - String normalized = new String(rootKey, StandardCharsets.UTF_8) - .replace('i', '\u0131'); // ı Turkish dotless-i - Assert.assertEquals("aaaaaaaı", normalized); - - // same result for lowercase input - byte[] rootKey2 = AccountIdIndexStore.getLowerCaseAccountId( - "aaaaaaai".getBytes(StandardCharsets.UTF_8)); - Assert.assertEquals(normalized, - new String(rootKey2, StandardCharsets.UTF_8) - .replace('i', '\u0131')); // ı Turkish dotless-i - - // #9c: no 'i' → normalized equals ROOT (no extra probe needed) - byte[] rootKeyNoI = AccountIdIndexStore.getLowerCaseAccountId( - "ABCDEFGH".getBytes(StandardCharsets.UTF_8)); - String normalizedNoI = new String(rootKeyNoI, StandardCharsets.UTF_8) - .replace('i', '\u0131'); // ı Turkish dotless-i - Assert.assertEquals("abcdefgh", normalizedNoI); - } - - /** - * Scenario #10: locale migration — all uppercase 'I'. - * Turkish node stored "BBBBBBBI" → key "bbbbbbbbı". - * All query case variants must find it. - * - *
-   * stored: "BBBBBBBI".toLower(TR) = "bbbbbbbı"
-   * +---+-----------+-------+--------+------------+--------+
-   * |   | query     | ROOT  | direct | normalized | result |
-   * +---+-----------+-------+--------+------------+--------+
-   * | a | "BBBBBBBI"| miss  | hit    | -          |  ✓     |
-   * | b | "bbbbbbbi"| miss  | skip   | hit        |  ✓     |
-   * | c | "BbBbBbBi"| miss  | miss   | hit        |  ✓     |
-   * +---+-----------+-------+--------+------------+--------+
-   * 
- */ - @Test - @SuppressWarnings("StringCaseLocaleUsage") - public void testLocaleMigrationAllUpperI() { - byte[] accountId = "BBBBBBBI".getBytes(StandardCharsets.UTF_8); - byte[] address = randomBytes(16); - - byte[] legacyKey = new String(accountId, StandardCharsets.UTF_8) - .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); - accountIdIndexStore.put(legacyKey, new BytesCapsule(address)); - - // (a) same-case query — found via direct fallback - Assert.assertTrue("has(BBBBBBBI)", accountIdIndexStore.has(accountId)); - Assert.assertArrayEquals("get(BBBBBBBI)", - address, accountIdIndexStore.get(ByteString.copyFrom(accountId))); - - // (b) all-lowercase query — found via normalized fallback - byte[] lowerQuery = "bbbbbbbi".getBytes(StandardCharsets.UTF_8); - Assert.assertTrue("has(bbbbbbbi)", accountIdIndexStore.has(lowerQuery)); - Assert.assertArrayEquals("get(bbbbbbbi)", - address, accountIdIndexStore.get(ByteString.copyFrom(lowerQuery))); - - // (c) mixed-case query (lowercase 'i') — found via normalized fallback - byte[] mixedQuery = "BbBbBbBi".getBytes(StandardCharsets.UTF_8); - Assert.assertTrue("has(BbBbBbBi)", accountIdIndexStore.has(mixedQuery)); - Assert.assertArrayEquals("get(BbBbBbBi)", - address, accountIdIndexStore.get(ByteString.copyFrom(mixedQuery))); - } - - /** - * Scenario #11: locale migration — two uppercase 'I' (like "AIBI"). - * All query case variants must find it. + * Scenario #9: verify which characters in the valid accountId range + * (0x21 '!' to 0x7E '~') differ between ROOT and Turkish toLowerCase. + * Expected: only 'I' (U+0049) differs. * - *
-   * stored: "DDIDDI".toLower(TR) = "ddıddı"
-   * +---+-----------+-------+--------+------------+--------+
-   * |   | query     | ROOT  | direct | normalized | result |
-   * +---+-----------+-------+--------+------------+--------+
-   * | a | "DDIDDI"  | miss  | hit    | -          |  ✓     |
-   * | b | "ddiddi"  | miss  | skip   | hit        |  ✓     |
-   * | c | "DdIddI"  | miss  | hit    | -          |  ✓     |
-   * | d | "DdiDdi"  | miss  | miss   | hit        |  ✓     |
-   * +---+-----------+-------+--------+------------+--------+
-   * 
+ * @see org.tron.core.utils.TransactionUtil#validReadableBytes */ @Test @SuppressWarnings("StringCaseLocaleUsage") - public void testLocaleMigrationMultipleUpperI() { - byte[] accountId = "DDIDDI".getBytes(StandardCharsets.UTF_8); - byte[] address = randomBytes(16); - - byte[] legacyKey = new String(accountId, StandardCharsets.UTF_8) - .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8); - accountIdIndexStore.put(legacyKey, new BytesCapsule(address)); + public void testTurkishLowerCaseDiffForValidAccountIdRange() { + StringBuilder diffChars = new StringBuilder(); + // 0x21 ('!') to 0x7E ('~') — the full valid accountId byte range + for (char c = 0x21; c <= 0x7E; c++) { + String s = String.valueOf(c); + String rootLower = s.toLowerCase(Locale.ROOT); + String turkishLower = s.toLowerCase(TURKISH); + if (!rootLower.equals(turkishLower)) { + diffChars.append(String.format( + "'%c'(0x%02X): ROOT='%s'(U+%04X) vs TR='%s'(U+%04X); ", + c, (int) c, + rootLower, (int) rootLower.charAt(0), + turkishLower, (int) turkishLower.charAt(0))); + } + } - // (a) same-case - Assert.assertArrayEquals("get(DDIDDI)", - address, accountIdIndexStore.get(ByteString.copyFrom(accountId))); - // (b) all-lowercase — normalized fallback - Assert.assertArrayEquals("get(ddiddi)", address, - accountIdIndexStore.get(ByteString.copyFrom( - "ddiddi".getBytes(StandardCharsets.UTF_8)))); - // (c) same uppercase I positions — direct fallback - Assert.assertArrayEquals("get(DdIddI)", address, - accountIdIndexStore.get(ByteString.copyFrom( - "DdIddI".getBytes(StandardCharsets.UTF_8)))); - // (d) all lowercase i — normalized fallback - Assert.assertArrayEquals("get(DdiDdi)", address, - accountIdIndexStore.get(ByteString.copyFrom( - "DdiDdi".getBytes(StandardCharsets.UTF_8)))); + String diff = diffChars.toString().trim(); + // Expect exactly one diff entry: 'I' + Assert.assertEquals( + "Only 'I' should differ in valid accountId range. Actual: " + diff, + "'I'(0x49): ROOT='i'(U+0069) vs TR='" + + "\u0131'(U+0131);", diff); // ı Turkish dotless-i } /** - * Scenario #12: Turkish key migration — mixed 'i' and 'I' (like "AIBi", "AiBI"). - * Before migration, cross-case query was a known limitation. - * After migrateTurkishKeys(), all Turkish keys are normalized to ROOT, - * so both same-case and cross-case queries work. + * Scenario #10: Turkish key migration — mixed 'i' and 'I' (like "AIBi", "AiBI"). + * After {@link MigrateTurkishKeyHelper#doWork()}, all Turkish keys are + * normalized to ROOT, so all query case variants work. * *
    * stored(1): "EEIEEi".toLower(TR) = "eeıeei" → migrated to "eeieei"
@@ -460,7 +334,7 @@ public void testMigrateTurkishKeysMixedCase() {
         .toLowerCase(TURKISH).getBytes(StandardCharsets.UTF_8);
     accountIdIndexStore.put(legacyKey2, new BytesCapsule(address2));
 
-    // Before migration: cross-case query misses (mixed i/ı key)
+    // Before migration: ROOT key lookup misses (key contains ı not i)
     Assert.assertFalse("pre-migrate: has(eeieei) should miss",
         accountIdIndexStore.has("eeieei".getBytes(StandardCharsets.UTF_8)));
     Assert.assertFalse("pre-migrate: has(ffiffi) should miss",
@@ -501,8 +375,8 @@ public void testMigrateTurkishKeysMixedCase() {
   }
 
   /**
-   * Scenario #13: accountId with only lowercase 'i' (like "Ai", "AiBi").
-   * Turkish node stored the same key as ROOT — no fallback needed.
+   * Scenario #11: accountId with only lowercase 'i' (like "Ai", "AiBi").
+   * Turkish node stored the same key as ROOT — no migration needed.
    */
   @Test
   @SuppressWarnings("StringCaseLocaleUsage")

From aa5489a55b58010945668f018383e33f88406588 Mon Sep 17 00:00:00 2001
From: halibobo1205 
Date: Mon, 13 Apr 2026 16:08:08 +0800
Subject: [PATCH 3/4] fix: replace @SuppressWarnings with Locale.ROOT in test
 classes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Replace remaining @SuppressWarnings("StringCaseLocaleUsage") with
explicit Locale.ROOT in 6 test files. The suppress annotations were
hiding real locale-sensitivity bugs:

- BytecodeCompiler: toUpperCase() on opcode names like "jumpi" would
  produce "JUMPİ" on Turkish locale, causing opcode lookup failure
- ShieldedReceiveTest: toUpperCase() on ztron address containing 'i'
  would produce 'İ' instead of 'I', breaking address decoding
- CronExpressionTest, OperationsTest, DataWord, SM2KeyTest: hex
  formatting with toUpperCase() (low risk but inconsistent)

Co-Authored-By: Claude Opus 4.6 
---
 .../java/org/tron/common/cron/CronExpressionTest.java    | 6 +++---
 .../src/test/java/org/tron/common/crypto/SM2KeyTest.java | 4 ++--
 .../org/tron/common/runtime/vm/BytecodeCompiler.java     | 4 ++--
 .../java/org/tron/common/runtime/vm/OperationsTest.java  | 9 ++++-----
 .../org/tron/common/utils/client/utils/DataWord.java     | 4 ++--
 .../java/org/tron/core/zksnark/ShieldedReceiveTest.java  | 4 ++--
 6 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java b/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java
index 37383690478..1c8bdf86134 100644
--- a/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java
+++ b/framework/src/test/java/org/tron/common/cron/CronExpressionTest.java
@@ -24,6 +24,7 @@
 import java.text.ParseException;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.Locale;
 import org.junit.Test;
 
 public class CronExpressionTest {
@@ -161,12 +162,11 @@ public void testLastDayOffset() throws Exception {
 
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   @Test
   public void testQuartz() throws Exception {
     CronExpression cronExpression = new CronExpression("19 15 10 4 Apr ? ");
-    assertEquals("19 15 10 4 Apr ? ".toUpperCase(), cronExpression.getCronExpression());
-    assertEquals("19 15 10 4 Apr ? ".toUpperCase(), cronExpression.toString());
+    assertEquals("19 15 10 4 Apr ? ".toUpperCase(Locale.ROOT), cronExpression.getCronExpression());
+    assertEquals("19 15 10 4 Apr ? ".toUpperCase(Locale.ROOT), cronExpression.toString());
 
     // if broken, this will throw an exception
     cronExpression.getNextValidTimeAfter(new Date());
diff --git a/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java b/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java
index 4fd0fed7f0b..b8507256ba3 100644
--- a/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java
+++ b/framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java
@@ -13,6 +13,7 @@
 import java.security.KeyPairGenerator;
 import java.security.SignatureException;
 import java.util.Arrays;
+import java.util.Locale;
 import lombok.extern.slf4j.Slf4j;
 import org.bouncycastle.crypto.digests.SM3Digest;
 import org.bouncycastle.util.encoders.Hex;
@@ -116,7 +117,6 @@ public void testInvalidSignatureLength() throws SignatureException {
     fail("Expecting a SignatureException for invalid signature length");
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   @Test
   public void testSM3Hash() {
     SM2 key = SM2.fromPublicOnly(pubKey);
@@ -124,7 +124,7 @@ public void testSM3Hash() {
     String message = "message digest";
     byte[] hash = signer.generateSM3Hash(message.getBytes());
     assertEquals("2A723761EAE35429DF643648FD69FB7787E7FC32F321BFAF7E294390F529BAF4",
-        Hex.toHexString(hash).toUpperCase());
+        Hex.toHexString(hash).toUpperCase(Locale.ROOT));
   }
 
 
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java b/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java
index 92d0da0c4d7..38e813719fc 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/BytecodeCompiler.java
@@ -2,6 +2,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 import org.bouncycastle.util.encoders.Hex;
 import org.tron.core.vm.Op;
@@ -12,13 +13,12 @@ public byte[] compile(String code) {
     return compile(code.split("\\s+"));
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   private byte[] compile(String[] tokens) {
     List bytecodes = new ArrayList<>();
     int ntokens = tokens.length;
 
     for (String s : tokens) {
-      String token = s.trim().toUpperCase();
+      String token = s.trim().toUpperCase(Locale.ROOT);
 
       if (token.isEmpty()) {
         continue;
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java
index 9a34f0a1434..db5cdd3f21a 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java
@@ -5,6 +5,7 @@
 import static org.tron.core.config.Parameter.ChainConstant.FROZEN_PERIOD;
 
 import java.util.List;
+import java.util.Locale;
 import java.util.Random;
 
 import lombok.SneakyThrows;
@@ -489,7 +490,6 @@ public void testBlockInformationOperations() throws ContractValidateException {
   }
 
   // test Memory, Storage and Flow Operations
-  @SuppressWarnings("StringCaseLocaleUsage")
   @Test
   public void testMemoryStorageAndFlowOperations() throws ContractValidateException {
     invoke = new ProgramInvokeMockImpl();
@@ -539,7 +539,7 @@ public void testMemoryStorageAndFlowOperations() throws ContractValidateExceptio
     testSingleOperation(program);
     Assert.assertEquals(20, program.getResult().getEnergyUsed());
     Assert.assertEquals("00000000000000000000000000000000000000000000000000000000000000CC",
-        Hex.toHexString(program.getStack().peek().getData()).toUpperCase());
+        Hex.toHexString(program.getStack().peek().getData()).toUpperCase(Locale.ROOT));
 
     // PC = 0x58
     op = new byte[]{0x60, 0x01, 0x60, 0x00, 0x58};
@@ -819,7 +819,6 @@ public void testOtherOperations() throws ContractValidateException {
     Assert.assertTrue(program.getResult().isRevert());
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   @Ignore
   @Test
   public void testComplexOperations() throws ContractValidateException {
@@ -863,7 +862,7 @@ public void testComplexOperations() throws ContractValidateException {
     testSingleOperation(program);
     Assert.assertEquals(10065, program.getResult().getEnergyUsed());
     Assert.assertEquals("0000000000000000000000000000000000000000000000000000000000000033",
-        Hex.toHexString(program.getStack().peek().getData()).toUpperCase());
+        Hex.toHexString(program.getStack().peek().getData()).toUpperCase(Locale.ROOT));
 
     // EXTCODESIZE = 0x3b
     op = new byte[]{0x3b};
@@ -883,7 +882,7 @@ public void testComplexOperations() throws ContractValidateException {
     testSingleOperation(program);
     Assert.assertEquals(38, program.getResult().getEnergyUsed());
     Assert.assertEquals("6000600000000000000000000000000000000000000000000000000000000000",
-        Hex.toHexString(program.getMemory()).toUpperCase());
+        Hex.toHexString(program.getMemory()).toUpperCase(Locale.ROOT));
 
   }
 
diff --git a/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java b/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java
index 56b765301ac..a719b7bb9af 100644
--- a/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java
+++ b/framework/src/test/java/org/tron/common/utils/client/utils/DataWord.java
@@ -24,6 +24,7 @@
 import com.fasterxml.jackson.annotation.JsonValue;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
+import java.util.Locale;
 
 import org.bouncycastle.util.Arrays;
 import org.bouncycastle.util.encoders.Hex;
@@ -430,9 +431,8 @@ public String toPrefixString() {
     return Hex.toHexString(pref).substring(0, 6);
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   public String shortHex() {
-    String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase();
+    String hexValue = Hex.toHexString(getNoLeadZeroesData()).toUpperCase(Locale.ROOT);
     return "0x" + hexValue.replaceFirst("^0+(?!$)", "");
   }
 
diff --git a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java
index 562af84cc4c..7143cef43e2 100755
--- a/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java
+++ b/framework/src/test/java/org/tron/core/zksnark/ShieldedReceiveTest.java
@@ -12,6 +12,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
 import javax.annotation.Resource;
@@ -2527,12 +2528,11 @@ public void pushSameSkAndScanAndSpend() throws Exception {
     Assert.assertTrue(ok2);
   }
 
-  @SuppressWarnings("StringCaseLocaleUsage")
   @Test
   public void decodePaymentAddressIgnoreCase() {
     String addressLower =
         "ztron1975m0wyg8f30cgf2l5fgndhzqzkzgkgnxge8cwx2wr7m3q7chsuwewh2e6u24yykum0hq8ue99u";
-    String addressUpper = addressLower.toUpperCase();
+    String addressUpper = addressLower.toUpperCase(Locale.ROOT);
 
     PaymentAddress paymentAddress1 = KeyIo.decodePaymentAddress(addressLower);
     PaymentAddress paymentAddress2 = KeyIo.decodePaymentAddress(addressUpper);

From 37b11b728c7c9fe68a25ce6846ea7187510a39f6 Mon Sep 17 00:00:00 2001
From: halibobo1205 
Date: Tue, 14 Apr 2026 11:00:07 +0800
Subject: [PATCH 4/4] test: add mainnet keys migration test with Turkish locale
 simulation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Simulate Turkish locale writes for all 13 mainnet accountId keys
(excluding the empty key), verify ROOT-based queries miss for keys
containing uppercase 'I', run MigrateTurkishKeyHelper, then verify
all queries (original case, lowercase, uppercase) succeed.

Keys with uppercase 'I' (BitTorrent, InfStonesSSRWallet, ISSRWallet,
JustDoIt, RtytIturtet) produce different Turkish keys (I→ı) and
require migration to restore ROOT-based lookups.

Co-Authored-By: Claude Opus 4.6 
---
 .../tron/core/db/AccountIdIndexStoreTest.java | 97 ++++++++++++++++++-
 1 file changed, 96 insertions(+), 1 deletion(-)

diff --git a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java
index 80323841214..9ab62d42d08 100644
--- a/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java
+++ b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java
@@ -375,7 +375,102 @@ public void testMigrateTurkishKeysMixedCase() {
   }
 
   /**
-   * Scenario #11: accountId with only lowercase 'i' (like "Ai", "AiBi").
+   * Scenario #11: mainnet data — simulate Turkish locale writes for all 14
+   * mainnet accountId keys, run migration, verify all queries succeed.
+   *
+   * 

Mainnet keys (already lowercase, as stored in DB): + * "", "12345678", "543838383", "bittorrent", "converse", "helloworld", + * "infstonessrwallet", "issrwallet", "justdoit", "justinsun", + * "justinsuntron", "rtytiturtet", "tronbetfestival", "vena_family" + * + *

Of these, 10 contain lowercase 'i'. On a Turkish node, the original + * accountId (which may have uppercase letters) would produce keys with + * 'ı' instead of 'i'. This test simulates that scenario. + * + *

+   * Phase 1: write keys as a Turkish node would (toLowerCase(TURKISH))
+   * Phase 2: verify ROOT-based queries miss for keys containing 'I'
+   * Phase 3: run MigrateTurkishKeyHelper
+   * Phase 4: verify all ROOT-based queries succeed
+   * 
+ */ + @Test + @SuppressWarnings("StringCaseLocaleUsage") + public void testMainnetKeysMigration() { + // Original accountIds as they might have been submitted (mixed case) + // The lowercase versions match the 14 mainnet keys observed in production + String[] mainnetAccountIds = { + "12345678", // no letters + "543838383", // no letters + "BitTorrent", // contains I → Turkish key differs + "Converse", // no I + "HelloWorld", // no I + "InfStonesSSRWallet", // contains I → Turkish key differs + "ISSRWallet", // contains I → Turkish key differs + "JustDoIt", // contains I → Turkish key differs + "JustinSun", // no I + "JustinSunTron", // no I + "RtytIturtet", // contains I → Turkish key differs + "TronBetFestival",// no I + "vena_family" // no I, all lowercase + }; + + byte[][] addresses = new byte[mainnetAccountIds.length][]; + byte[][] turkishKeys = new byte[mainnetAccountIds.length][]; + + // Phase 1: simulate Turkish node writes + for (int i = 0; i < mainnetAccountIds.length; i++) { + addresses[i] = randomBytes(16); + String turkishLower = mainnetAccountIds[i].toLowerCase(TURKISH); + turkishKeys[i] = turkishLower.getBytes(StandardCharsets.UTF_8); + accountIdIndexStore.put(turkishKeys[i], new BytesCapsule(addresses[i])); + } + + // Phase 2: verify which keys are findable via ROOT lookup before migration + for (int i = 0; i < mainnetAccountIds.length; i++) { + String rootLower = mainnetAccountIds[i].toLowerCase(Locale.ROOT); + String turkishLower = mainnetAccountIds[i].toLowerCase(TURKISH); + boolean shouldMiss = !rootLower.equals(turkishLower); + if (shouldMiss) { + Assert.assertNull( + "pre-migrate: ROOT query should miss for " + mainnetAccountIds[i], + accountIdIndexStore.get(ByteString.copyFrom( + mainnetAccountIds[i].getBytes(StandardCharsets.UTF_8)))); + } else { + Assert.assertArrayEquals( + "pre-migrate: ROOT query should hit for " + mainnetAccountIds[i], + addresses[i], + accountIdIndexStore.get(ByteString.copyFrom( + mainnetAccountIds[i].getBytes(StandardCharsets.UTF_8)))); + } + } + + // Phase 3: run migration + new MigrateTurkishKeyHelper(chainBaseManager).doWork(); + + // Phase 4: verify ALL queries succeed after migration + for (int i = 0; i < mainnetAccountIds.length; i++) { + // Original case query + Assert.assertArrayEquals( + "post-migrate: get(" + mainnetAccountIds[i] + ")", + addresses[i], + accountIdIndexStore.get(ByteString.copyFrom( + mainnetAccountIds[i].getBytes(StandardCharsets.UTF_8)))); + // All-lowercase query + String lower = mainnetAccountIds[i].toLowerCase(Locale.ROOT); + Assert.assertTrue( + "post-migrate: has(" + lower + ")", + accountIdIndexStore.has(lower.getBytes(StandardCharsets.UTF_8))); + // All-uppercase query + String upper = mainnetAccountIds[i].toUpperCase(Locale.ROOT); + Assert.assertTrue( + "post-migrate: has(" + upper + ")", + accountIdIndexStore.has(upper.getBytes(StandardCharsets.UTF_8))); + } + } + + /** + * Scenario #12: accountId with only lowercase 'i' (like "Ai", "AiBi"). * Turkish node stored the same key as ROOT — no migration needed. */ @Test