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..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,6 +1,7 @@ package org.tron.core.store; import com.google.protobuf.ByteString; +import java.util.Locale; import java.util.Objects; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -19,9 +20,10 @@ public AccountIdIndexStore(@Value("accountid-index") String dbName) { super(dbName); } - private static byte[] getLowerCaseAccountId(byte[] bsAccountId) { + public static byte[] getLowerCaseAccountId(byte[] bsAccountId) { return ByteString - .copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase()).toByteArray(); + .copyFromUtf8(ByteString.copyFrom(bsAccountId).toStringUtf8().toLowerCase(Locale.ROOT)) + .toByteArray(); } public void put(AccountCapsule accountCapsule) { @@ -54,4 +56,4 @@ public boolean has(byte[] key) { return !ArrayUtils.isEmpty(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..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 { @@ -164,8 +165,8 @@ public void testLastDayOffset() throws Exception { @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 87e4e14698c..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; @@ -123,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 48355f137f4..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; @@ -17,7 +18,7 @@ private byte[] compile(String[] tokens) { 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 583b0131942..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; @@ -538,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}; @@ -861,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}; @@ -881,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 f676ef6c8e4..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; @@ -431,7 +432,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/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java b/framework/src/test/java/org/tron/core/db/AccountIdIndexStoreTest.java index 236c3464697..9ab62d42d08 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 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 { 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,27 +131,28 @@ 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(); - 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()) + .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()) + .copyFromUtf8(ByteString.copyFrom(accountName).toStringUtf8().toUpperCase(Locale.ROOT)) .toByteArray(); result = accountIdIndexStore.has(upperCase); Assert.assertTrue("upperCase fail", result); @@ -128,4 +160,342 @@ 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); + } + + /** + * Scenario #8: Turkish direct key — toLowerCase(TURKISH) produces + * different results for inputs containing uppercase 'I'. + * + *
+   * +-------+----------------+---------------------+
+   * | 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: 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 #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"
+   * 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: 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", + 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 #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 + @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..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; @@ -2531,7 +2532,7 @@ public void pushSameSkAndScanAndSpend() throws Exception { 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); 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'