From d9f784ed2b6a92c687eaea582d9822461fa6a06f Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 13 May 2020 10:19:56 +0100 Subject: [PATCH 1/4] Registered names: changing 'owner' and allowing renaming. REGISTER_NAME has an "owner" field which can be different from the actual registrant (transaction creator's public key, used for signing transaction). This allowed people to register names to be owned by someone else, thus breaking the whole "one name per account" aspect. So now "owner" is removed from REGISTER_NAME, and the actual owner address is derived from transaction creator's public key, as you would expect. Similarly, UPDATE_NAME has a corresponding "newOwner" field which has been removed. In addition, UPDATE_NAME now allows users to change their registered name using a new "newName" field. Various changes made to DB, Name class, etc. to accomodate above, along with some minor bug-fixes and comment improvements/corrections. Needs new unit tests to cover both new functionality and old! --- .../java/org/qortal/data/naming/NameData.java | 4 ++ .../RegisterNameTransactionData.java | 9 +--- .../UpdateNameTransactionData.java | 20 ++++---- src/main/java/org/qortal/naming/Name.java | 38 ++++++++++----- .../hsqldb/HSQLDBDatabaseUpdates.java | 4 +- ...QLDBRegisterNameTransactionRepository.java | 12 ++--- ...HSQLDBUpdateNameTransactionRepository.java | 10 ++-- .../transaction/BuyNameTransaction.java | 18 ++++--- .../transaction/RegisterNameTransaction.java | 21 ++++---- .../transaction/UpdateNameTransaction.java | 48 ++++++++++++------- .../RegisterNameTransactionTransformer.java | 7 +-- .../UpdateNameTransactionTransformer.java | 22 +++++---- .../org/qortal/test/api/NamesApiTests.java | 6 +-- .../RegisterNameTestTransaction.java | 3 +- .../org/qortal/test/naming/BuySellTests.java | 2 +- .../org/qortal/test/naming/MiscTests.java | 2 +- 16 files changed, 123 insertions(+), 103 deletions(-) diff --git a/src/main/java/org/qortal/data/naming/NameData.java b/src/main/java/org/qortal/data/naming/NameData.java index dffe830e..8fc05c79 100644 --- a/src/main/java/org/qortal/data/naming/NameData.java +++ b/src/main/java/org/qortal/data/naming/NameData.java @@ -66,6 +66,10 @@ public class NameData { return this.name; } + public void setName(String name) { + this.name = name; + } + public String getData() { return this.data; } diff --git a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java index 37cc65d2..f111acf6 100644 --- a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java @@ -19,8 +19,6 @@ public class RegisterNameTransactionData extends TransactionData { // Properties @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] registrantPublicKey; - @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") - private String owner; @Schema(description = "requested name", example = "my-name") private String name; @Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }") @@ -38,11 +36,10 @@ public class RegisterNameTransactionData extends TransactionData { } /** From repository */ - public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String owner, String name, String data) { + public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) { super(TransactionType.REGISTER_NAME, baseTransactionData); this.registrantPublicKey = baseTransactionData.creatorPublicKey; - this.owner = owner; this.name = name; this.data = data; } @@ -53,10 +50,6 @@ public class RegisterNameTransactionData extends TransactionData { return this.registrantPublicKey; } - public String getOwner() { - return this.owner; - } - public String getName() { return this.name; } diff --git a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java index 70c7404d..93c94c8e 100644 --- a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java @@ -17,10 +17,10 @@ public class UpdateNameTransactionData extends TransactionData { // Properties @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] ownerPublicKey; - @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") - private String newOwner; @Schema(description = "which name to update", example = "my-name") private String name; + @Schema(description = "new name", example = "my-new-name") + private String newName; @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }") private String newData; // For internal use when orphaning @@ -40,19 +40,19 @@ public class UpdateNameTransactionData extends TransactionData { } /** From repository */ - public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData, byte[] nameReference) { + public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData, byte[] nameReference) { super(TransactionType.UPDATE_NAME, baseTransactionData); this.ownerPublicKey = baseTransactionData.creatorPublicKey; - this.newOwner = newOwner; this.name = name; + this.newName = newName; this.newData = newData; this.nameReference = nameReference; } /** From network/API */ - public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String newOwner, String name, String newData) { - this(baseTransactionData, newOwner, name, newData, null); + public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) { + this(baseTransactionData, name, newName, newData, null); } // Getters / setters @@ -61,14 +61,14 @@ public class UpdateNameTransactionData extends TransactionData { return this.ownerPublicKey; } - public String getNewOwner() { - return this.newOwner; - } - public String getName() { return this.name; } + public String getNewName() { + return this.newName; + } + public String getNewData() { return this.newData; } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index b377907f..17d1aae2 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -3,6 +3,7 @@ package org.qortal.naming; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; import org.qortal.data.transaction.CancelSellNameTransactionData; @@ -20,6 +21,7 @@ public class Name { private NameData nameData; // Useful constants + public static final int MIN_NAME_SIZE = 3; public static final int MAX_NAME_SIZE = 400; public static final int MAX_DATA_SIZE = 4000; @@ -33,7 +35,10 @@ public class Name { */ public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) { this.repository = repository; - this.nameData = new NameData(registerNameTransactionData.getOwner(), + + String owner = Crypto.toAddress(registerNameTransactionData.getRegistrantPublicKey()); + + this.nameData = new NameData(owner, registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), registerNameTransactionData.getSignature(), registerNameTransactionData.getTxGroupId()); } @@ -66,23 +71,31 @@ public class Name { throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); switch (previousTransactionData.getType()) { - case REGISTER_NAME: + case REGISTER_NAME: { RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; - this.nameData.setOwner(previousRegisterNameTransactionData.getOwner()); + this.nameData.setName(previousRegisterNameTransactionData.getName()); this.nameData.setData(previousRegisterNameTransactionData.getData()); break; + } - case UPDATE_NAME: + case UPDATE_NAME: { UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData; - this.nameData.setData(previousUpdateNameTransactionData.getNewData()); - this.nameData.setOwner(previousUpdateNameTransactionData.getNewOwner()); - break; - case BUY_NAME: + if (!previousUpdateNameTransactionData.getNewName().isBlank()) + this.nameData.setName(previousUpdateNameTransactionData.getNewName()); + + if (!previousUpdateNameTransactionData.getNewData().isEmpty()) + this.nameData.setData(previousUpdateNameTransactionData.getNewData()); + + break; + } + + case BUY_NAME: { BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey()); this.nameData.setOwner(buyer.getAddress()); break; + } default: throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); @@ -96,9 +109,12 @@ public class Name { // New name reference is this transaction's signature this.nameData.setReference(updateNameTransactionData.getSignature()); - // Update Name's owner and data - this.nameData.setOwner(updateNameTransactionData.getNewOwner()); - this.nameData.setData(updateNameTransactionData.getNewData()); + // Update name and data where appropriate + if (!updateNameTransactionData.getNewName().isEmpty()) + this.nameData.setOwner(updateNameTransactionData.getNewName()); + + if (!updateNameTransactionData.getNewData().isEmpty()) + this.nameData.setData(updateNameTransactionData.getNewData()); // Save updated name data this.repository.getNameRepository().save(this.nameData); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 730740cd..58af6c3d 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -294,11 +294,11 @@ public class HSQLDBDatabaseUpdates { // Register Name Transactions stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "owner QortalAddress NOT NULL, data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); + + "data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); // Update Name Transactions stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); + + "new_name RegisteredName NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); // Sell Name Transactions stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java index bddeaf07..bf13aeb3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java @@ -17,17 +17,16 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT owner, name, data FROM RegisterNameTransactions WHERE signature = ?"; + String sql = "SELECT name, data FROM RegisterNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; - String owner = resultSet.getString(1); - String name = resultSet.getString(2); - String data = resultSet.getString(3); + String name = resultSet.getString(1); + String data = resultSet.getString(2); - return new RegisterNameTransactionData(baseTransactionData, owner, name, data); + return new RegisterNameTransactionData(baseTransactionData, name, data); } catch (SQLException e) { throw new DataException("Unable to fetch register name transaction from repository", e); } @@ -40,8 +39,7 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe HSQLDBSaver saveHelper = new HSQLDBSaver("RegisterNameTransactions"); saveHelper.bind("signature", registerNameTransactionData.getSignature()).bind("registrant", registerNameTransactionData.getRegistrantPublicKey()) - .bind("owner", registerNameTransactionData.getOwner()).bind("name", registerNameTransactionData.getName()) - .bind("data", registerNameTransactionData.getData()); + .bind("name", registerNameTransactionData.getName()).bind("data", registerNameTransactionData.getData()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java index d0df6ff9..773ed5e4 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java @@ -17,18 +17,18 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT new_owner, name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?"; + String sql = "SELECT name, new_name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; - String newOwner = resultSet.getString(1); - String name = resultSet.getString(2); + String name = resultSet.getString(1); + String newName = resultSet.getString(2); String newData = resultSet.getString(3); byte[] nameReference = resultSet.getBytes(4); - return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData, nameReference); + return new UpdateNameTransactionData(baseTransactionData, name, newName, newData, nameReference); } catch (SQLException e) { throw new DataException("Unable to fetch update name transaction from repository", e); } @@ -41,7 +41,7 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo HSQLDBSaver saveHelper = new HSQLDBSaver("UpdateNameTransactions"); saveHelper.bind("signature", updateNameTransactionData.getSignature()).bind("owner", updateNameTransactionData.getOwnerPublicKey()) - .bind("new_owner", updateNameTransactionData.getNewOwner()).bind("name", updateNameTransactionData.getName()) + .bind("name", updateNameTransactionData.getName()).bind("new_name", updateNameTransactionData.getNewName()) .bind("new_data", updateNameTransactionData.getNewData()).bind("name_reference", updateNameTransactionData.getNameReference()); try { diff --git a/src/main/java/org/qortal/transaction/BuyNameTransaction.java b/src/main/java/org/qortal/transaction/BuyNameTransaction.java index 63b6447e..e2be539f 100644 --- a/src/main/java/org/qortal/transaction/BuyNameTransaction.java +++ b/src/main/java/org/qortal/transaction/BuyNameTransaction.java @@ -5,6 +5,7 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; @@ -54,7 +55,7 @@ public class BuyNameTransaction extends Transaction { // Check name size bounds int nameLength = Utf8.encodedLength(name); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check name is lowercase @@ -76,6 +77,11 @@ public class BuyNameTransaction extends Transaction { if (buyer.getAddress().equals(nameData.getOwner())) return ValidationResult.BUYER_ALREADY_OWNER; + // If accounts are only allowed one registered name then check for this + if (BlockChain.getInstance().oneNamePerAccount() + && !this.repository.getNameRepository().getNamesByOwner(buyer.getAddress()).isEmpty()) + return ValidationResult.MULTIPLE_NAMES_FORBIDDEN; + // Check expected seller currently owns name if (!this.buyNameTransactionData.getSeller().equals(nameData.getOwner())) return ValidationResult.INVALID_SELLER; @@ -84,7 +90,7 @@ public class BuyNameTransaction extends Transaction { if (this.buyNameTransactionData.getAmount() != nameData.getSalePrice()) return ValidationResult.INVALID_AMOUNT; - // Check issuer has enough funds + // Check buyer has enough funds if (buyer.getConfirmedBalance(Asset.QORT) < this.buyNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; @@ -93,21 +99,21 @@ public class BuyNameTransaction extends Transaction { @Override public void process() throws DataException { - // Update Name + // Buy Name Name name = new Name(this.repository, this.buyNameTransactionData.getName()); name.buy(this.buyNameTransactionData); - // Save transaction with updated "name reference" pointing to previous transaction that updated name + // Save transaction with updated "name reference" pointing to previous transaction that changed name this.repository.getTransactionRepository().save(this.buyNameTransactionData); } @Override public void orphan() throws DataException { - // Revert name + // Un-buy name Name name = new Name(this.repository, this.buyNameTransactionData.getName()); name.unbuy(this.buyNameTransactionData); - // Save this transaction, with removed "name reference" + // Save this transaction, with previous "name reference" this.repository.getTransactionRepository().save(this.buyNameTransactionData); } diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 053413eb..5587d0ec 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -6,7 +6,6 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.block.BlockChain; -import org.qortal.crypto.Crypto; import org.qortal.data.transaction.RegisterNameTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.naming.Name; @@ -32,7 +31,7 @@ public class RegisterNameTransaction extends Transaction { @Override public List getRecipientAddresses() throws DataException { - return Collections.singletonList(this.registerNameTransactionData.getOwner()); + return Collections.emptyList(); } // Navigation @@ -46,23 +45,20 @@ public class RegisterNameTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { Account registrant = getRegistrant(); - - // Check owner address is valid - if (!Crypto.isValidAddress(this.registerNameTransactionData.getOwner())) - return ValidationResult.INVALID_ADDRESS; + String name = this.registerNameTransactionData.getName(); // Check name size bounds - int nameLength = Utf8.encodedLength(this.registerNameTransactionData.getName()); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + int nameLength = Utf8.encodedLength(name); + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; // Check data size bounds int dataLength = Utf8.encodedLength(this.registerNameTransactionData.getData()); - if (dataLength < 1 || dataLength > Name.MAX_DATA_SIZE) + if (dataLength > Name.MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; // Check name is lowercase - if (!this.registerNameTransactionData.getName().equals(this.registerNameTransactionData.getName().toLowerCase())) + if (!name.equals(name.toLowerCase())) return ValidationResult.NAME_NOT_LOWER_CASE; // Check registrant has enough funds @@ -78,10 +74,9 @@ public class RegisterNameTransaction extends Transaction { if (this.repository.getNameRepository().nameExists(this.registerNameTransactionData.getName())) return ValidationResult.NAME_ALREADY_REGISTERED; - Account registrant = getRegistrant(); - // If accounts are only allowed one registered name then check for this - if (BlockChain.getInstance().oneNamePerAccount() && !this.repository.getNameRepository().getNamesByOwner(registrant.getAddress()).isEmpty()) + if (BlockChain.getInstance().oneNamePerAccount() + && !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty()) return ValidationResult.MULTIPLE_NAMES_FORBIDDEN; return ValidationResult.OK; diff --git a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java index 2b731bd1..8ecc7472 100644 --- a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java @@ -5,7 +5,6 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; -import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.UpdateNameTransactionData; @@ -32,7 +31,7 @@ public class UpdateNameTransaction extends Transaction { @Override public List getRecipientAddresses() throws DataException { - return Collections.singletonList(this.updateNameTransactionData.getNewOwner()); + return Collections.emptyList(); } // Navigation @@ -41,30 +40,17 @@ public class UpdateNameTransaction extends Transaction { return this.getCreator(); } - public Account getNewOwner() { - return new Account(this.repository, this.updateNameTransactionData.getNewOwner()); - } - // Processing @Override public ValidationResult isValid() throws DataException { String name = this.updateNameTransactionData.getName(); - // Check new owner address is valid - if (!Crypto.isValidAddress(this.updateNameTransactionData.getNewOwner())) - return ValidationResult.INVALID_ADDRESS; - // Check name size bounds int nameLength = Utf8.encodedLength(name); - if (nameLength < 1 || nameLength > Name.MAX_NAME_SIZE) + if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check new data size bounds - int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData()); - if (newDataLength < 1 || newDataLength > Name.MAX_DATA_SIZE) - return ValidationResult.INVALID_DATA_LENGTH; - // Check name is lowercase if (!name.equals(name.toLowerCase())) return ValidationResult.NAME_NOT_LOWER_CASE; @@ -79,6 +65,24 @@ public class UpdateNameTransaction extends Transaction { if (nameData.getCreationGroupId() != this.updateNameTransactionData.getTxGroupId()) return ValidationResult.TX_GROUP_ID_MISMATCH; + // Check new name (0 length means don't update name) + String newName = this.updateNameTransactionData.getNewName(); + int newNameLength = Utf8.encodedLength(newName); + if (newNameLength != 0) { + // Check new name size bounds + if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check new name is lowercase + if (!newName.equals(newName.toLowerCase())) + return ValidationResult.NAME_NOT_LOWER_CASE; + } + + // Check new data size bounds (0 length means don't update data) + int newDataLength = Utf8.encodedLength(this.updateNameTransactionData.getNewData()); + if (newDataLength > Name.MAX_DATA_SIZE) + return ValidationResult.INVALID_DATA_LENGTH; + Account owner = getOwner(); // Check owner has enough funds @@ -92,6 +96,10 @@ public class UpdateNameTransaction extends Transaction { public ValidationResult isProcessable() throws DataException { NameData nameData = this.repository.getNameRepository().fromName(this.updateNameTransactionData.getName()); + // Check name still exists + if (nameData == null) + return ValidationResult.NAME_DOES_NOT_EXIST; + // Check name isn't currently for sale if (nameData.getIsForSale()) return ValidationResult.NAME_ALREADY_FOR_SALE; @@ -102,6 +110,10 @@ public class UpdateNameTransaction extends Transaction { if (!owner.getAddress().equals(nameData.getOwner())) return ValidationResult.INVALID_NAME_OWNER; + // Check new name isn't already taken + if (this.repository.getNameRepository().nameExists(this.updateNameTransactionData.getNewName())) + return ValidationResult.NAME_ALREADY_REGISTERED; + return ValidationResult.OK; } @@ -111,7 +123,7 @@ public class UpdateNameTransaction extends Transaction { Name name = new Name(this.repository, this.updateNameTransactionData.getName()); name.update(this.updateNameTransactionData); - // Save this transaction, now with updated "name reference" to previous transaction that updated name + // Save this transaction, now with updated "name reference" to previous transaction that changed name this.repository.getTransactionRepository().save(this.updateNameTransactionData); } @@ -121,7 +133,7 @@ public class UpdateNameTransaction extends Transaction { Name name = new Name(this.repository, this.updateNameTransactionData.getName()); name.revert(this.updateNameTransactionData); - // Save this transaction, now with removed "name reference" + // Save this transaction, with previous "name reference" this.repository.getTransactionRepository().save(this.updateNameTransactionData); } diff --git a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java index 1bf4057e..6722c601 100644 --- a/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/RegisterNameTransactionTransformer.java @@ -33,7 +33,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { layout.add("transaction's groupID", TransformationType.INT); layout.add("reference", TransformationType.SIGNATURE); layout.add("name registrant's public key", TransformationType.PUBLIC_KEY); - layout.add("name owner", TransformationType.ADDRESS); layout.add("name length", TransformationType.INT); layout.add("name", TransformationType.STRING); layout.add("data length", TransformationType.INT); @@ -52,8 +51,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { byte[] registrantPublicKey = Serialization.deserializePublicKey(byteBuffer); - String owner = Serialization.deserializeAddress(byteBuffer); - String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); @@ -65,7 +62,7 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, registrantPublicKey, fee, signature); - return new RegisterNameTransactionData(baseTransactionData, owner, name, data); + return new RegisterNameTransactionData(baseTransactionData, name, data); } public static int getDataLength(TransactionData transactionData) throws TransformationException { @@ -83,8 +80,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { transformCommonBytes(transactionData, bytes); - Serialization.serializeAddress(bytes, registerNameTransactionData.getOwner()); - Serialization.serializeSizedString(bytes, registerNameTransactionData.getName()); Serialization.serializeSizedString(bytes, registerNameTransactionData.getData()); diff --git a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java index 56a4f988..6e3aa06b 100644 --- a/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java +++ b/src/main/java/org/qortal/transform/transaction/UpdateNameTransactionTransformer.java @@ -18,11 +18,11 @@ import com.google.common.primitives.Longs; public class UpdateNameTransactionTransformer extends TransactionTransformer { // Property lengths - private static final int OWNER_LENGTH = ADDRESS_LENGTH; private static final int NAME_SIZE_LENGTH = INT_LENGTH; - private static final int DATA_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_NAME_SIZE_LENGTH = INT_LENGTH; + private static final int NEW_DATA_SIZE_LENGTH = INT_LENGTH; - private static final int EXTRAS_LENGTH = OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH; + private static final int EXTRAS_LENGTH = NAME_SIZE_LENGTH + NEW_NAME_SIZE_LENGTH + NEW_DATA_SIZE_LENGTH; protected static final TransactionLayout layout; @@ -33,10 +33,11 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { layout.add("transaction's groupID", TransformationType.INT); layout.add("reference", TransformationType.SIGNATURE); layout.add("name owner's public key", TransformationType.PUBLIC_KEY); - layout.add("name's new owner", TransformationType.ADDRESS); layout.add("name length", TransformationType.INT); layout.add("name", TransformationType.STRING); - layout.add("new data length", TransformationType.INT); + layout.add("new name's length (0 for no change)", TransformationType.INT); + layout.add("new name", TransformationType.STRING); + layout.add("new data length (0 for no change)", TransformationType.INT); layout.add("new data", TransformationType.STRING); layout.add("fee", TransformationType.AMOUNT); layout.add("signature", TransformationType.SIGNATURE); @@ -52,10 +53,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { byte[] ownerPublicKey = Serialization.deserializePublicKey(byteBuffer); - String newOwner = Serialization.deserializeAddress(byteBuffer); - String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); + String newName = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); + String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); long fee = byteBuffer.getLong(); @@ -65,13 +66,14 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, ownerPublicKey, fee, signature); - return new UpdateNameTransactionData(baseTransactionData, newOwner, name, newData); + return new UpdateNameTransactionData(baseTransactionData, name, newName, newData); } public static int getDataLength(TransactionData transactionData) throws TransformationException { UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; return getBaseLength(transactionData) + EXTRAS_LENGTH + Utf8.encodedLength(updateNameTransactionData.getName()) + + Utf8.encodedLength(updateNameTransactionData.getNewName()) + Utf8.encodedLength(updateNameTransactionData.getNewData()); } @@ -83,10 +85,10 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { transformCommonBytes(transactionData, bytes); - Serialization.serializeAddress(bytes, updateNameTransactionData.getNewOwner()); - Serialization.serializeSizedString(bytes, updateNameTransactionData.getName()); + Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewName()); + Serialization.serializeSizedString(bytes, updateNameTransactionData.getNewData()); bytes.write(Longs.toByteArray(updateNameTransactionData.getFee())); diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java index ae7248b4..2d31c8c2 100644 --- a/src/test/java/org/qortal/test/api/NamesApiTests.java +++ b/src/test/java/org/qortal/test/api/NamesApiTests.java @@ -46,7 +46,7 @@ public class NamesApiTests extends ApiCommon { PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null)); @@ -61,7 +61,7 @@ public class NamesApiTests extends ApiCommon { PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); assertNotNull(this.namesResource.getName(name)); @@ -76,7 +76,7 @@ public class NamesApiTests extends ApiCommon { String name = "test-name"; long price = 1_23456789L; - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); // Sell-name diff --git a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java index 14f93657..0450fbee 100644 --- a/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java +++ b/src/test/java/org/qortal/test/common/transaction/RegisterNameTestTransaction.java @@ -9,14 +9,13 @@ import org.qortal.repository.Repository; public class RegisterNameTestTransaction extends TestTransaction { public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException { - String owner = account.getAddress(); String name = "test name"; if (!wantValid) name += " " + random.nextInt(1_000_000); String data = "{ \"key\": \"value\" }"; - return new RegisterNameTransactionData(generateBase(account), owner, name, data); + return new RegisterNameTransactionData(generateBase(account), name, data); } } diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java index 0d323c5b..23022613 100644 --- a/src/test/java/org/qortal/test/naming/BuySellTests.java +++ b/src/test/java/org/qortal/test/naming/BuySellTests.java @@ -61,7 +61,7 @@ public class BuySellTests extends Common { @Test public void testRegisterName() throws DataException { // Register-name - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); String name = transactionData.getName(); diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index d9cbf6fc..d183ed82 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -29,7 +29,7 @@ public class MiscTests extends Common { PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); TransactionUtils.signAndMint(repository, transactionData, alice); List recentNames = repository.getNameRepository().getRecentNames(0L); From cea0cee9a8e2a38af13f403f3185ce1810e05327 Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 13 May 2020 15:07:36 +0100 Subject: [PATCH 2/4] Names: fixes to allow name change and tests to cover --- src/main/java/org/qortal/naming/Name.java | 26 ++- .../transaction/UpdateNameTransaction.java | 7 +- .../org/qortal/test/naming/MiscTests.java | 170 ++++++++++++++++++ 3 files changed, 194 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index 17d1aae2..75f620b7 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -70,11 +70,15 @@ public class Name { if (previousTransactionData == null) throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); + String previousName = this.nameData.getName(); + switch (previousTransactionData.getType()) { case REGISTER_NAME: { RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; + this.nameData.setName(previousRegisterNameTransactionData.getName()); this.nameData.setData(previousRegisterNameTransactionData.getData()); + break; } @@ -92,14 +96,22 @@ public class Name { case BUY_NAME: { BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; + Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey()); this.nameData.setOwner(buyer.getAddress()); + break; } default: throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); } + + this.repository.getNameRepository().save(this.nameData); + + if (!this.nameData.getName().equals(previousName)) + // Name has changed, delete old entry + this.repository.getNameRepository().delete(previousName); } public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException { @@ -110,8 +122,12 @@ public class Name { this.nameData.setReference(updateNameTransactionData.getSignature()); // Update name and data where appropriate - if (!updateNameTransactionData.getNewName().isEmpty()) - this.nameData.setOwner(updateNameTransactionData.getNewName()); + if (!updateNameTransactionData.getNewName().isEmpty()) { + // If we're changing the name, we need to delete old entry + this.repository.getNameRepository().delete(nameData.getName()); + + this.nameData.setName(updateNameTransactionData.getNewName()); + } if (!updateNameTransactionData.getNewData().isEmpty()) this.nameData.setData(updateNameTransactionData.getNewData()); @@ -127,9 +143,6 @@ public class Name { // Previous Name's owner and/or data taken from referenced transaction this.revert(); - // Save reverted name data - this.repository.getNameRepository().save(this.nameData); - // Remove reference to previous name-changing transaction updateNameTransactionData.setNameReference(null); } @@ -213,9 +226,6 @@ public class Name { // Revert seller's balance Account seller = new Account(this.repository, this.nameData.getOwner()); seller.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount()); - - // Save reverted name data - this.repository.getNameRepository().save(this.nameData); } } diff --git a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java index 8ecc7472..379b4e08 100644 --- a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java @@ -130,7 +130,12 @@ public class UpdateNameTransaction extends Transaction { @Override public void orphan() throws DataException { // Revert name - Name name = new Name(this.repository, this.updateNameTransactionData.getName()); + + String nameToRevert = this.updateNameTransactionData.getNewName(); + if (nameToRevert.isEmpty()) + nameToRevert = this.updateNameTransactionData.getName(); + + Name name = new Name(this.repository, nameToRevert); name.revert(this.updateNameTransactionData); // Save this transaction, with previous "name reference" diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index d183ed82..f124b2dc 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -8,12 +8,17 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; import org.qortal.test.common.Common; import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; public class MiscTests extends Common { @@ -39,4 +44,169 @@ public class MiscTests extends Common { } } + @Test + public void testUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = "new-name"; + String newData = ""; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(name)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check old name exists again + assertTrue(repository.getNameRepository().nameExists(name)); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testDoubleUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = "new-name"; + String newData = ""; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(name)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + String newestName = "newest-name"; + String newestData = "abc"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check previous name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check newest name exists + assertTrue(repository.getNameRepository().nameExists(newestName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check newest name no longer exists + assertFalse(repository.getNameRepository().nameExists(newestName)); + + // Check previous name exists again + assertTrue(repository.getNameRepository().nameExists(newName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check original name exists again + assertTrue(repository.getNameRepository().nameExists(name)); + } + } + + @Test + public void testUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{}"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = ""; + String newData = "new-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(name)); + + // Check data is correct + assertEquals(newData, repository.getNameRepository().fromName(name).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(name)); + + // Check old data restored + assertEquals(data, repository.getNameRepository().fromName(name).getData()); + } + } + + // test trying to register same name twice + @Test + public void testDuplicateRegisterName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // duplicate + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", ValidationResult.OK != result); + } + } + + // test register then trying to update another name to existing name + @Test + public void testUpdateToExistingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = "new-name"; + String newData = ""; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // we shouldn't be able to update name to existing name + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, name, newData); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", ValidationResult.OK != result); + } + } + } From 197c742ce7a8b63d25415f1c0a68aeb93f25837d Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 15 May 2020 14:08:46 +0100 Subject: [PATCH 3/4] Major work on Registered Names Changes include: * Allowing renaming * Tracking last-updated timestamps * More stringent Unicode processing * Way more unit tests * Max name length reduction to 40 chars Note: HSQLDB repository table changes --- pom.xml | 6 + .../java/org/qortal/data/naming/NameData.java | 81 +++-- .../RegisterNameTransactionData.java | 26 +- .../UpdateNameTransactionData.java | 24 +- src/main/java/org/qortal/naming/Name.java | 175 +++++---- .../org/qortal/repository/NameRepository.java | 4 + .../hsqldb/HSQLDBDatabaseUpdates.java | 12 +- .../hsqldb/HSQLDBNameRepository.java | 117 ++++-- ...QLDBRegisterNameTransactionRepository.java | 10 +- ...HSQLDBUpdateNameTransactionRepository.java | 10 +- .../transaction/BuyNameTransaction.java | 2 +- .../CancelSellNameTransaction.java | 2 +- .../transaction/RegisterNameTransaction.java | 19 +- .../transaction/SellNameTransaction.java | 2 +- .../transaction/UpdateNameTransaction.java | 30 +- src/main/java/org/qortal/utils/Unicode.java | 220 ++++++++++++ .../java/org/qortal/test/UnicodeTests.java | 38 ++ .../org/qortal/test/naming/BuySellTests.java | 32 +- .../org/qortal/test/naming/MiscTests.java | 143 +------- .../org/qortal/test/naming/UpdateTests.java | 334 ++++++++++++++++++ 20 files changed, 982 insertions(+), 305 deletions(-) create mode 100644 src/main/java/org/qortal/utils/Unicode.java create mode 100644 src/test/java/org/qortal/test/UnicodeTests.java create mode 100644 src/test/java/org/qortal/test/naming/UpdateTests.java diff --git a/pom.xml b/pom.xml index 379a7ce8..3f67c2f5 100644 --- a/pom.xml +++ b/pom.xml @@ -504,6 +504,12 @@ mail 1.5.0-b01 + + + net.codebox + homoglyph + 1.2.0 + org.eclipse.jetty diff --git a/src/main/java/org/qortal/data/naming/NameData.java b/src/main/java/org/qortal/data/naming/NameData.java index 8fc05c79..83681712 100644 --- a/src/main/java/org/qortal/data/naming/NameData.java +++ b/src/main/java/org/qortal/data/naming/NameData.java @@ -12,18 +12,29 @@ import io.swagger.v3.oas.annotations.media.Schema; public class NameData { // Properties - private String owner; + private String name; + + private String reducedName; + + private String owner; + private String data; + private long registered; - private Long updated; - // No need to expose this via API + + private Long updated; // Not always present + + private boolean isForSale; + + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + private Long salePrice; + + // For internal use - no need to expose this via API @XmlTransient @Schema(hidden = true) private byte[] reference; - private boolean isForSale; - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private Long salePrice; + // For internal use @XmlTransient @Schema(hidden = true) @@ -31,14 +42,17 @@ public class NameData { // Constructors - // necessary for JAX-RS serialization + // necessary for JAXB protected NameData() { } - public NameData(String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, Long salePrice, - int creationGroupId) { - this.owner = owner; + // Typically used when fetching from repository + public NameData(String name, String reducedName, String owner, String data, long registered, + Long updated, boolean isForSale, Long salePrice, + byte[] reference, int creationGroupId) { this.name = name; + this.reducedName = reducedName; + this.owner = owner; this.data = data; this.registered = registered; this.updated = updated; @@ -48,20 +62,13 @@ public class NameData { this.creationGroupId = creationGroupId; } - public NameData(String owner, String name, String data, long registered, byte[] reference, int creationGroupId) { - this(owner, name, data, registered, null, reference, false, null, creationGroupId); + // Typically used when registering a new name + public NameData(String name, String reducedName, String owner, String data, long registered, byte[] reference, int creationGroupId) { + this(name, reducedName, owner, data, registered, null, false, null, reference, creationGroupId); } // Getters / setters - public String getOwner() { - return this.owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - public String getName() { return this.name; } @@ -70,6 +77,22 @@ public class NameData { this.name = name; } + public String getReducedName() { + return this.reducedName; + } + + public void setReducedName(String reducedName) { + this.reducedName = reducedName; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + public String getData() { return this.data; } @@ -90,15 +113,7 @@ public class NameData { this.updated = updated; } - public byte[] getReference() { - return this.reference; - } - - public void setReference(byte[] reference) { - this.reference = reference; - } - - public boolean getIsForSale() { + public boolean isForSale() { return this.isForSale; } @@ -114,6 +129,14 @@ public class NameData { this.salePrice = salePrice; } + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + public int getCreationGroupId() { return this.creationGroupId; } diff --git a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java index f111acf6..6f0ef214 100644 --- a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java @@ -3,8 +3,10 @@ package org.qortal.data.transaction; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; +import org.qortal.naming.Name; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -17,13 +19,21 @@ import io.swagger.v3.oas.annotations.media.Schema; public class RegisterNameTransactionData extends TransactionData { // Properties + @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] registrantPublicKey; + @Schema(description = "requested name", example = "my-name") private String name; + @Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }") private String data; + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedName; + // Constructors // For JAXB @@ -36,12 +46,18 @@ public class RegisterNameTransactionData extends TransactionData { } /** From repository */ - public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) { + public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data, String reducedName) { super(TransactionType.REGISTER_NAME, baseTransactionData); this.registrantPublicKey = baseTransactionData.creatorPublicKey; this.name = name; this.data = data; + this.reducedName = reducedName; + } + + /** From network */ + public RegisterNameTransactionData(BaseTransactionData baseTransactionData, String name, String data) { + this(baseTransactionData, name, data, Name.reduceName(name)); } // Getters / setters @@ -58,4 +74,12 @@ public class RegisterNameTransactionData extends TransactionData { return this.data; } + public String getReducedName() { + return this.reducedName; + } + + public void setReducedName(String reducedName) { + this.reducedName = reducedName; + } + } diff --git a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java index 93c94c8e..5d7e449a 100644 --- a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java @@ -5,6 +5,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; +import org.qortal.naming.Name; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -15,14 +16,24 @@ import io.swagger.v3.oas.annotations.media.Schema; public class UpdateNameTransactionData extends TransactionData { // Properties + @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] ownerPublicKey; + @Schema(description = "which name to update", example = "my-name") private String name; + @Schema(description = "new name", example = "my-new-name") private String newName; + @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }") private String newData; + + // For internal use + @XmlTransient + @Schema(hidden = true) + private String reducedNewName; + // For internal use when orphaning @XmlTransient @Schema(hidden = true) @@ -40,19 +51,20 @@ public class UpdateNameTransactionData extends TransactionData { } /** From repository */ - public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData, byte[] nameReference) { + public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData, String reducedNewName, byte[] nameReference) { super(TransactionType.UPDATE_NAME, baseTransactionData); this.ownerPublicKey = baseTransactionData.creatorPublicKey; this.name = name; this.newName = newName; this.newData = newData; + this.reducedNewName = reducedNewName; this.nameReference = nameReference; } /** From network/API */ public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) { - this(baseTransactionData, name, newName, newData, null); + this(baseTransactionData, name, newName, newData, Name.reduceName(newName), null); } // Getters / setters @@ -73,6 +85,14 @@ public class UpdateNameTransactionData extends TransactionData { return this.newData; } + public String getReducedNewName() { + return this.reducedNewName; + } + + public void setReducedNewName(String reducedNewName) { + this.reducedNewName = reducedNewName; + } + public byte[] getNameReference() { return this.nameReference; } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index 75f620b7..b7570552 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -13,6 +13,8 @@ import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.transaction.Transaction.TransactionType; +import org.qortal.utils.Unicode; public class Name { @@ -22,7 +24,7 @@ public class Name { // Useful constants public static final int MIN_NAME_SIZE = 3; - public static final int MAX_NAME_SIZE = 400; + public static final int MAX_NAME_SIZE = 40; public static final int MAX_DATA_SIZE = 4000; // Constructors @@ -37,9 +39,10 @@ public class Name { this.repository = repository; String owner = Crypto.toAddress(registerNameTransactionData.getRegistrantPublicKey()); + String reducedName = Unicode.sanitize(registerNameTransactionData.getName()); - this.nameData = new NameData(owner, - registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), + this.nameData = new NameData(registerNameTransactionData.getName(), reducedName, owner, + registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), registerNameTransactionData.getSignature(), registerNameTransactionData.getTxGroupId()); } @@ -57,6 +60,10 @@ public class Name { // Processing + public static String reduceName(String name) { + return Unicode.sanitize(name); + } + public void register() throws DataException { this.repository.getNameRepository().save(this.nameData); } @@ -65,55 +72,6 @@ public class Name { this.repository.getNameRepository().delete(this.nameData.getName()); } - private void revert() throws DataException { - TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(this.nameData.getReference()); - if (previousTransactionData == null) - throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); - - String previousName = this.nameData.getName(); - - switch (previousTransactionData.getType()) { - case REGISTER_NAME: { - RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; - - this.nameData.setName(previousRegisterNameTransactionData.getName()); - this.nameData.setData(previousRegisterNameTransactionData.getData()); - - break; - } - - case UPDATE_NAME: { - UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData; - - if (!previousUpdateNameTransactionData.getNewName().isBlank()) - this.nameData.setName(previousUpdateNameTransactionData.getNewName()); - - if (!previousUpdateNameTransactionData.getNewData().isEmpty()) - this.nameData.setData(previousUpdateNameTransactionData.getNewData()); - - break; - } - - case BUY_NAME: { - BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; - - Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey()); - this.nameData.setOwner(buyer.getAddress()); - - break; - } - - default: - throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); - } - - this.repository.getNameRepository().save(this.nameData); - - if (!this.nameData.getName().equals(previousName)) - // Name has changed, delete old entry - this.repository.getNameRepository().delete(previousName); - } - public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException { // Update reference in transaction data updateNameTransactionData.setNameReference(this.nameData.getReference()); @@ -121,12 +79,15 @@ public class Name { // New name reference is this transaction's signature this.nameData.setReference(updateNameTransactionData.getSignature()); + // Set name's last-updated timestamp + this.nameData.setUpdated(updateNameTransactionData.getTimestamp()); + // Update name and data where appropriate if (!updateNameTransactionData.getNewName().isEmpty()) { - // If we're changing the name, we need to delete old entry - this.repository.getNameRepository().delete(nameData.getName()); - this.nameData.setName(updateNameTransactionData.getNewName()); + + // If we're changing the name, we need to delete old entry + this.repository.getNameRepository().delete(updateNameTransactionData.getName()); } if (!updateNameTransactionData.getNewData().isEmpty()) @@ -138,15 +99,68 @@ public class Name { public void revert(UpdateNameTransactionData updateNameTransactionData) throws DataException { // Previous name reference is taken from this transaction's cached copy - this.nameData.setReference(updateNameTransactionData.getNameReference()); + byte[] nameReference = updateNameTransactionData.getNameReference(); - // Previous Name's owner and/or data taken from referenced transaction - this.revert(); + // Revert name's name-changing transaction reference + this.nameData.setReference(nameReference); + + // Revert name's last-updated timestamp + this.nameData.setUpdated(fetchPreviousUpdateTimestamp(nameReference)); + + // We can find previous 'name' from update transaction + this.nameData.setName(updateNameTransactionData.getName()); + + // We might need to hunt for previous data value + if (!updateNameTransactionData.getNewData().isEmpty()) + this.nameData.setData(findPreviousData(nameReference)); + + this.repository.getNameRepository().save(this.nameData); + + if (!updateNameTransactionData.getNewName().isEmpty()) + // Name has changed, delete old entry + this.repository.getNameRepository().delete(updateNameTransactionData.getNewName()); // Remove reference to previous name-changing transaction updateNameTransactionData.setNameReference(null); } + private String findPreviousData(byte[] nameReference) throws DataException { + // Follow back through name-references until we hit the data we need + while (true) { + TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference); + if (previousTransactionData == null) + throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); + + switch (previousTransactionData.getType()) { + case REGISTER_NAME: { + RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData; + + return previousRegisterNameTransactionData.getData(); + } + + case UPDATE_NAME: { + UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData; + + if (!previousUpdateNameTransactionData.getNewData().isEmpty()) + return previousUpdateNameTransactionData.getNewData(); + + nameReference = previousUpdateNameTransactionData.getNameReference(); + + break; + } + + case BUY_NAME: { + BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData; + nameReference = previousBuyNameTransactionData.getNameReference(); + break; + } + + default: + throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction"); + } + } + } + public void sell(SellNameTransactionData sellNameTransactionData) throws DataException { // Mark as for-sale and set price this.nameData.setIsForSale(true); @@ -182,6 +196,10 @@ public class Name { } public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException { + // Save previous name-changing reference in this transaction's data + // Caller is expected to save + buyNameTransactionData.setNameReference(this.nameData.getReference()); + // Mark not for-sale but leave price in case we want to orphan this.nameData.setIsForSale(false); @@ -195,12 +213,12 @@ public class Name { // Update buyer's balance buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount()); - // Update reference in transaction data - buyNameTransactionData.setNameReference(this.nameData.getReference()); - - // New name reference is this transaction's signature + // Set name-changing reference to this transaction this.nameData.setReference(buyNameTransactionData.getSignature()); + // Set name's last-updated timestamp + this.nameData.setUpdated(buyNameTransactionData.getTimestamp()); + // Save updated name data this.repository.getNameRepository().save(this.nameData); } @@ -210,22 +228,41 @@ public class Name { this.nameData.setIsForSale(true); this.nameData.setSalePrice(buyNameTransactionData.getAmount()); - // Previous name reference is taken from this transaction's cached copy + // Previous name-changing reference is taken from this transaction's cached copy this.nameData.setReference(buyNameTransactionData.getNameReference()); - // Remove reference in transaction data - buyNameTransactionData.setNameReference(null); + // Revert name's last-updated timestamp + this.nameData.setUpdated(fetchPreviousUpdateTimestamp(buyNameTransactionData.getNameReference())); + + // Revert to previous owner + this.nameData.setOwner(buyNameTransactionData.getSeller()); + + // Save updated name data + this.repository.getNameRepository().save(this.nameData); // Revert buyer's balance Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey()); buyer.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount()); - // Previous Name's owner and/or data taken from referenced transaction - this.revert(); - // Revert seller's balance - Account seller = new Account(this.repository, this.nameData.getOwner()); + Account seller = new Account(this.repository, buyNameTransactionData.getSeller()); seller.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount()); + + // Clean previous name-changing reference from this transaction's data + // Caller is expected to save + buyNameTransactionData.setNameReference(null); + } + + private Long fetchPreviousUpdateTimestamp(byte[] nameReference) throws DataException { + TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(nameReference); + if (previousTransactionData == null) + throw new DataException("Unable to revert name transaction as referenced transaction not found in repository"); + + // If we've hit REGISTER_NAME then we've run out of updates + if (previousTransactionData.getType() == TransactionType.REGISTER_NAME) + return null; + + return previousTransactionData.getTimestamp(); } } diff --git a/src/main/java/org/qortal/repository/NameRepository.java b/src/main/java/org/qortal/repository/NameRepository.java index e056fb1d..d6c0f33e 100644 --- a/src/main/java/org/qortal/repository/NameRepository.java +++ b/src/main/java/org/qortal/repository/NameRepository.java @@ -10,6 +10,10 @@ public interface NameRepository { public boolean nameExists(String name) throws DataException; + public NameData fromReducedName(String reducedName) throws DataException; + + public boolean reducedNameExists(String reducedName) throws DataException; + public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException; public default List getAllNames() throws DataException { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 58af6c3d..22f828f5 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -285,20 +285,22 @@ public class HSQLDBDatabaseUpdates { case 8: // Name-related - stmt.execute("CREATE TABLE Names (name RegisteredName, owner QortalAddress NOT NULL, " - + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, creation_group_id GroupID NOT NULL DEFAULT 0, " - + "reference Signature, is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + stmt.execute("CREATE TABLE Names (name RegisteredName, reduced_name RegisteredName, owner QortalAddress NOT NULL, " + + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, " + + "is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + + "reference Signature, creation_group_id GroupID NOT NULL DEFAULT 0, " + "PRIMARY KEY (name))"); // For finding names by owner stmt.execute("CREATE INDEX NamesOwnerIndex ON Names (owner)"); // Register Name Transactions stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); + + "data NameData NOT NULL, reduced_name RegisteredName NOT NULL, " + TRANSACTION_KEYS + ")"); // Update Name Transactions stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_name RegisteredName NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); + + "new_name RegisteredName NOT NULL, new_data NameData NOT NULL, reduced_new_name RegisteredName NOT NULL, " + + "name_reference Signature, " + TRANSACTION_KEYS + ")"); // Sell Name Transactions stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index 3f6c9030..3a3574ef 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -19,31 +19,33 @@ public class HSQLDBNameRepository implements NameRepository { @Override public NameData fromName(String name) throws DataException { - String sql = "SELECT owner, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; + String sql = "SELECT reduced_name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) { if (resultSet == null) return null; - String owner = resultSet.getString(1); - String data = resultSet.getString(2); - long registered = resultSet.getLong(3); + String reducedName = resultSet.getString(1); + String owner = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(4); + Long updated = resultSet.getLong(5); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - return new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId); + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch name info from repository", e); } @@ -58,11 +60,55 @@ public class HSQLDBNameRepository implements NameRepository { } } + @Override + public NameData fromReducedName(String reducedName) throws DataException { + String sql = "SELECT name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE reduced_name = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, reducedName)) { + if (resultSet == null) + return null; + + String name = resultSet.getString(1); + String owner = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + + boolean isForSale = resultSet.getBoolean(6); + + Long salePrice = resultSet.getLong(7); + if (salePrice == 0 && resultSet.wasNull()) + salePrice = null; + + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); + + return new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId); + } catch (SQLException e) { + throw new DataException("Unable to fetch name info from repository", e); + } + } + + @Override + public boolean reducedNameExists(String reducedName) throws DataException { + try { + return this.repository.exists("Names", "reduced_name = ?", reducedName); + } catch (SQLException e) { + throw new DataException("Unable to check for reduced name in repository", e); + } + } + @Override public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT name, data, owner, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -77,25 +123,26 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); + String reducedName = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = resultSet.getLong(4); + String data = resultSet.getString(4); + long registered = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(5); + Long updated = resultSet.getLong(6); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(6); boolean isForSale = resultSet.getBoolean(7); Long salePrice = resultSet.getLong(8); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(9); + byte[] reference = resultSet.getBytes(9); + int creationGroupId = resultSet.getInt(10); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -108,7 +155,8 @@ public class HSQLDBNameRepository implements NameRepository { public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, owner, registered_when, updated_when, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + sql.append("SELECT name, reduced_name, owner, data, registered_when, updated_when, " + + "sale_price, reference, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -123,25 +171,26 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); + String reducedName = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = resultSet.getLong(4); + String data = resultSet.getString(4); + long registered = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(5); + Long updated = resultSet.getLong(6); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(6); boolean isForSale = true; Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -154,7 +203,8 @@ public class HSQLDBNameRepository implements NameRepository { public List getNamesByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + sql.append("SELECT name, reduced_name, data, registered_when, updated_when, " + + "is_for_sale, sale_price, reference, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -169,24 +219,25 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); - String data = resultSet.getString(2); - long registered = resultSet.getLong(3); + String reducedName = resultSet.getString(2); + String data = resultSet.getString(3); + long registered = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Long updated = resultSet.getLong(4); + Long updated = resultSet.getLong(5); if (updated == 0 && resultSet.wasNull()) updated = null; - byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); Long salePrice = resultSet.getLong(7); if (salePrice == 0 && resultSet.wasNull()) salePrice = null; - int creationGroupId = resultSet.getInt(8); + byte[] reference = resultSet.getBytes(8); + int creationGroupId = resultSet.getInt(9); - names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice, creationGroupId)); + names.add(new NameData(name, reducedName, owner, data, registered, updated, isForSale, salePrice, reference, creationGroupId)); } while (resultSet.next()); return names; @@ -223,11 +274,11 @@ public class HSQLDBNameRepository implements NameRepository { public void save(NameData nameData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); - saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData()) + saveHelper.bind("name", nameData.getName()).bind("reduced_name", nameData.getReducedName()) + .bind("owner", nameData.getOwner()).bind("data", nameData.getData()) .bind("registered_when", nameData.getRegistered()).bind("updated_when", nameData.getUpdated()) - .bind("reference", nameData.getReference()) - .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()) - .bind("creation_group_id", nameData.getCreationGroupId()); + .bind("is_for_sale", nameData.isForSale()).bind("sale_price", nameData.getSalePrice()) + .bind("reference", nameData.getReference()).bind("creation_group_id", nameData.getCreationGroupId()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java index bf13aeb3..82da794f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBRegisterNameTransactionRepository.java @@ -17,16 +17,17 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT name, data FROM RegisterNameTransactions WHERE signature = ?"; + String sql = "SELECT name, reduced_name, data FROM RegisterNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) return null; String name = resultSet.getString(1); - String data = resultSet.getString(2); + String reducedName = resultSet.getString(2); + String data = resultSet.getString(3); - return new RegisterNameTransactionData(baseTransactionData, name, data); + return new RegisterNameTransactionData(baseTransactionData, name, data, reducedName); } catch (SQLException e) { throw new DataException("Unable to fetch register name transaction from repository", e); } @@ -39,7 +40,8 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe HSQLDBSaver saveHelper = new HSQLDBSaver("RegisterNameTransactions"); saveHelper.bind("signature", registerNameTransactionData.getSignature()).bind("registrant", registerNameTransactionData.getRegistrantPublicKey()) - .bind("name", registerNameTransactionData.getName()).bind("data", registerNameTransactionData.getData()); + .bind("name", registerNameTransactionData.getName()).bind("data", registerNameTransactionData.getData()) + .bind("reduced_name", registerNameTransactionData.getReducedName()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java index 773ed5e4..447ab5c6 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBUpdateNameTransactionRepository.java @@ -17,7 +17,7 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo } TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT name, new_name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?"; + String sql = "SELECT name, new_name, new_data, reduced_new_name, name_reference FROM UpdateNameTransactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { if (resultSet == null) @@ -26,9 +26,10 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo String name = resultSet.getString(1); String newName = resultSet.getString(2); String newData = resultSet.getString(3); - byte[] nameReference = resultSet.getBytes(4); + String reducedNewName = resultSet.getString(4); + byte[] nameReference = resultSet.getBytes(5); - return new UpdateNameTransactionData(baseTransactionData, name, newName, newData, nameReference); + return new UpdateNameTransactionData(baseTransactionData, name, newName, newData, reducedNewName, nameReference); } catch (SQLException e) { throw new DataException("Unable to fetch update name transaction from repository", e); } @@ -42,7 +43,8 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo saveHelper.bind("signature", updateNameTransactionData.getSignature()).bind("owner", updateNameTransactionData.getOwnerPublicKey()) .bind("name", updateNameTransactionData.getName()).bind("new_name", updateNameTransactionData.getNewName()) - .bind("new_data", updateNameTransactionData.getNewData()).bind("name_reference", updateNameTransactionData.getNameReference()); + .bind("new_data", updateNameTransactionData.getNewData()).bind("reduced_new_name", updateNameTransactionData.getReducedNewName()) + .bind("name_reference", updateNameTransactionData.getNameReference()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/transaction/BuyNameTransaction.java b/src/main/java/org/qortal/transaction/BuyNameTransaction.java index e2be539f..3b58842d 100644 --- a/src/main/java/org/qortal/transaction/BuyNameTransaction.java +++ b/src/main/java/org/qortal/transaction/BuyNameTransaction.java @@ -69,7 +69,7 @@ public class BuyNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name is currently for sale - if (!nameData.getIsForSale()) + if (!nameData.isForSale()) return ValidationResult.NAME_NOT_FOR_SALE; // Check buyer isn't trying to buy own name diff --git a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java index 18852c56..da81e4ea 100644 --- a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java @@ -62,7 +62,7 @@ public class CancelSellNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name is currently for sale - if (!nameData.getIsForSale()) + if (!nameData.isForSale()) return ValidationResult.NAME_NOT_FOR_SALE; // Check transaction creator matches name's current owner diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 5587d0ec..c0d91f0b 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -11,6 +11,7 @@ import org.qortal.data.transaction.TransactionData; import org.qortal.naming.Name; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; import com.google.common.base.Utf8; @@ -40,6 +41,15 @@ public class RegisterNameTransaction extends Transaction { return this.getCreator(); } + private synchronized String getReducedName() { + if (this.registerNameTransactionData.getReducedName() == null) { + String reducedName = Name.reduceName(this.registerNameTransactionData.getName()); + this.registerNameTransactionData.setReducedName(reducedName); + } + + return this.registerNameTransactionData.getReducedName(); + } + // Processing @Override @@ -57,21 +67,24 @@ public class RegisterNameTransaction extends Transaction { if (dataLength > Name.MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; - // Check name is lowercase - if (!name.equals(name.toLowerCase())) + // Check name is in normalized form (no leading/trailing whitespace, etc.) + if (!name.equals(Unicode.normalize(name))) return ValidationResult.NAME_NOT_LOWER_CASE; // Check registrant has enough funds if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Fill in missing reduced name. Caller is likely to save this as next step. + getReducedName(); + return ValidationResult.OK; } @Override public ValidationResult isProcessable() throws DataException { // Check the name isn't already taken - if (this.repository.getNameRepository().nameExists(this.registerNameTransactionData.getName())) + if (this.repository.getNameRepository().reducedNameExists(getReducedName())) return ValidationResult.NAME_ALREADY_REGISTERED; // If accounts are only allowed one registered name then check for this diff --git a/src/main/java/org/qortal/transaction/SellNameTransaction.java b/src/main/java/org/qortal/transaction/SellNameTransaction.java index 8694e66d..5b77097f 100644 --- a/src/main/java/org/qortal/transaction/SellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/SellNameTransaction.java @@ -65,7 +65,7 @@ public class SellNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name isn't currently for sale - if (nameData.getIsForSale()) + if (nameData.isForSale()) return ValidationResult.NAME_ALREADY_FOR_SALE; // Check transaction's public key matches name's current owner diff --git a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java index 379b4e08..f4cd3020 100644 --- a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java @@ -11,6 +11,7 @@ import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.naming.Name; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.Unicode; import com.google.common.base.Utf8; @@ -40,6 +41,15 @@ public class UpdateNameTransaction extends Transaction { return this.getCreator(); } + private synchronized String getReducedNewName() { + if (this.updateNameTransactionData.getReducedNewName() == null) { + String reducedNewName = Name.reduceName(this.updateNameTransactionData.getNewName()); + this.updateNameTransactionData.setReducedNewName(reducedNewName); + } + + return this.updateNameTransactionData.getReducedNewName(); + } + // Processing @Override @@ -51,8 +61,8 @@ public class UpdateNameTransaction extends Transaction { if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check name is lowercase - if (!name.equals(name.toLowerCase())) + // Check name is in normalized form (no leading/trailing whitespace, etc.) + if (!name.equals(Unicode.normalize(name))) return ValidationResult.NAME_NOT_LOWER_CASE; NameData nameData = this.repository.getNameRepository().fromName(name); @@ -73,8 +83,8 @@ public class UpdateNameTransaction extends Transaction { if (newNameLength < Name.MIN_NAME_SIZE || newNameLength > Name.MAX_NAME_SIZE) return ValidationResult.INVALID_NAME_LENGTH; - // Check new name is lowercase - if (!newName.equals(newName.toLowerCase())) + // Check new name is in normalized form (no leading/trailing whitespace, etc.) + if (!newName.equals(Unicode.normalize(newName))) return ValidationResult.NAME_NOT_LOWER_CASE; } @@ -89,6 +99,9 @@ public class UpdateNameTransaction extends Transaction { if (owner.getConfirmedBalance(Asset.QORT) < this.updateNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Fill in missing reduced new name. Caller is likely to save this as next step. + getReducedNewName(); + return ValidationResult.OK; } @@ -101,7 +114,7 @@ public class UpdateNameTransaction extends Transaction { return ValidationResult.NAME_DOES_NOT_EXIST; // Check name isn't currently for sale - if (nameData.getIsForSale()) + if (nameData.isForSale()) return ValidationResult.NAME_ALREADY_FOR_SALE; Account owner = getOwner(); @@ -110,8 +123,9 @@ public class UpdateNameTransaction extends Transaction { if (!owner.getAddress().equals(nameData.getOwner())) return ValidationResult.INVALID_NAME_OWNER; - // Check new name isn't already taken - if (this.repository.getNameRepository().nameExists(this.updateNameTransactionData.getNewName())) + // Check new name isn't already taken, unless it is the same name (this allows for case-adjusting renames) + NameData newNameData = this.repository.getNameRepository().fromReducedName(getReducedNewName()); + if (newNameData != null && !newNameData.getName().equals(nameData.getName())) return ValidationResult.NAME_ALREADY_REGISTERED; return ValidationResult.OK; @@ -129,7 +143,7 @@ public class UpdateNameTransaction extends Transaction { @Override public void orphan() throws DataException { - // Revert name + // Revert update String nameToRevert = this.updateNameTransactionData.getNewName(); if (nameToRevert.isEmpty()) diff --git a/src/main/java/org/qortal/utils/Unicode.java b/src/main/java/org/qortal/utils/Unicode.java new file mode 100644 index 00000000..8a9092ea --- /dev/null +++ b/src/main/java/org/qortal/utils/Unicode.java @@ -0,0 +1,220 @@ +package org.qortal.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.TreeMap; + +import com.google.common.base.CharMatcher; + +import net.codebox.homoglyph.HomoglyphBuilder; + +public abstract class Unicode { + + public static final String NO_BREAK_SPACE = "\u00a0"; + + public static final String ZERO_WIDTH_SPACE = "\u200b"; + public static final String ZERO_WIDTH_NON_JOINER = "\u200c"; + public static final String ZERO_WIDTH_JOINER = "\u200d"; + public static final String WORD_JOINER = "\u2060"; + public static final String ZERO_WIDTH_NO_BREAK_SPACE = "\ufeff"; + + public static final CharMatcher ZERO_WIDTH_CHAR_MATCHER = CharMatcher.anyOf(ZERO_WIDTH_SPACE + ZERO_WIDTH_NON_JOINER + ZERO_WIDTH_JOINER + WORD_JOINER + ZERO_WIDTH_NO_BREAK_SPACE); + + private static int[] homoglyphCodePoints; + private static int[] reducedCodePoints; + + private static final String CHAR_CODES_FILE = "/char_codes.txt"; + + static { + buildHomoglyphCodePointArrays(); + } + + /** Returns string in Unicode canonical normalized form (NFC),
+ * with zero-width spaces/joiners removed,
+ * leading/trailing whitespace trimmed
+ * and all other whitespace blocks collapsed into a single space character. + *

+ * Example: [ZWS] means zero-width space + *

    + *
  • " powdered [TAB] to[ZWS]ast " becomes "powdered toast"
  • + *
+ *

+ * @see Form#NFKC + * @see Unicode#removeZeroWidth(String) + * @see CharMatcher#whitespace() + * @see CharMatcher#trimAndCollapseFrom(CharSequence, char) + */ + public static String normalize(String input) { + String output; + + // Normalize + output = Normalizer.normalize(input, Form.NFKC); + + // Remove zero-width code-points, used for rendering + output = removeZeroWidth(output); + + // Normalize whitespace + output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' '); + + return output; + } + + /** Returns string after normalization,
+ * conversion to lowercase (locale insensitive)
+ * and homoglyphs replaced with simpler, reduced codepoints. + *

+ * Example: + *

    + *
  • " TΟÁST " becomes "toast" + *
+ *

+ * @see Form#NFKC + * @see Unicode#removeZeroWidth(String) + * @see CharMatcher#whitespace() + * @see CharMatcher#trimAndCollapseFrom(CharSequence, char) + * @see String#toLowerCase(Locale) + * @see Locale#ROOT + * @see Unicode#reduceHomoglyphs(String) + */ + public static String sanitize(String input) { + String output; + + // Normalize + output = Normalizer.normalize(input, Form.NFKD); + + // Remove zero-width code-points, used for rendering + output = removeZeroWidth(output); + + // Normalize whitespace + output = CharMatcher.whitespace().trimAndCollapseFrom(output, ' '); + + // Remove accents, combining marks + output = output.replaceAll("[\\p{M}\\p{C}]", ""); + + // Convert to lowercase + output = output.toLowerCase(Locale.ROOT); + + // Reduce homoglyphs + output = reduceHomoglyphs(output); + + return output; + } + + public static String removeZeroWidth(String input) { + return ZERO_WIDTH_CHAR_MATCHER.removeFrom(input); + } + + public static String reduceHomoglyphs(String input) { + CodePoints codePoints = new CodePoints(input); + final int length = codePoints.getLength(); + + for (int i = 0; i < length; ++i) { + int inputCodePoint = codePoints.getValue(i); + + int index = Arrays.binarySearch(homoglyphCodePoints, inputCodePoint); + if (index >= 0) + codePoints.setValue(i, reducedCodePoints[index]); + } + + return codePoints.toString(); + } + + private static void buildHomoglyphCodePointArrays() { + final InputStream is = HomoglyphBuilder.class.getResourceAsStream(CHAR_CODES_FILE); + + if (is == null) + throw new MissingResourceException("Unable to read " + CHAR_CODES_FILE, HomoglyphBuilder.class.getName(), + CHAR_CODES_FILE); + + final Reader reader = new InputStreamReader(is); + + Map homoglyphReductions = new TreeMap<>(); + + try (final BufferedReader bufferedReader = new BufferedReader(reader)) { + String line; + + while ((line = bufferedReader.readLine()) != null) { + line = line.trim(); + + if (line.startsWith("#") || line.length() == 0) + continue; + + String[] charCodes = line.split(","); + + // We consider the first charCode to be the 'reduced' form + int reducedCodepoint; + try { + reducedCodepoint = Integer.parseInt(charCodes[0], 16); + } catch (NumberFormatException ex) { + // ignore badly formatted lines + continue; + } + + // Map remaining charCodes + for (int i = 1; i < charCodes.length; ++i) + try { + int homoglyphCodepoint = Integer.parseInt(charCodes[i], 16); + + homoglyphReductions.put(homoglyphCodepoint, reducedCodepoint); + } catch (NumberFormatException ex) { + // ignore + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + homoglyphCodePoints = homoglyphReductions.keySet().stream().mapToInt(i -> i).toArray(); + reducedCodePoints = homoglyphReductions.values().stream().mapToInt(i -> i).toArray(); + } + + private static class CodePoints { + private final int[] codepointArray; + + public CodePoints(String text) { + final List codepointList = new ArrayList<>(); + + int codepoint; + for (int offset = 0; offset < text.length(); offset += Character.charCount(codepoint)) { + codepoint = text.codePointAt(offset); + codepointList.add(codepoint); + } + + this.codepointArray = codepointList.stream().mapToInt(i -> i).toArray(); + } + + public int getValue(int i) { + return codepointArray[i]; + } + + public void setValue(int i, int codepoint) { + codepointArray[i] = codepoint; + } + + public int getLength() { + return codepointArray.length; + } + + public String toString() { + final StringBuilder sb = new StringBuilder(this.codepointArray.length); + + for (int i = 0; i < this.codepointArray.length; i++) + sb.appendCodePoint(this.codepointArray[i]); + + return sb.toString(); + } + } + +} diff --git a/src/test/java/org/qortal/test/UnicodeTests.java b/src/test/java/org/qortal/test/UnicodeTests.java new file mode 100644 index 00000000..2e0f7968 --- /dev/null +++ b/src/test/java/org/qortal/test/UnicodeTests.java @@ -0,0 +1,38 @@ +package org.qortal.test; + +import static org.junit.Assert.*; +import static org.qortal.utils.Unicode.*; + +import org.junit.Test; +import org.qortal.utils.Unicode; + +public class UnicodeTests { + + @Test + public void testWhitespace() { + String input = " " + NO_BREAK_SPACE + "test "; + + String output = Unicode.normalize(input); + + assertEquals("trim & collapse failed", "test", output); + } + + @Test + public void testCaseComparison() { + String input1 = " " + NO_BREAK_SPACE + "test "; + String input2 = " " + NO_BREAK_SPACE + "TEST " + ZERO_WIDTH_SPACE; + + assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2)); + } + + @Test + public void testHomoglyph() { + String omicron = "\u03bf"; + + String input1 = " " + NO_BREAK_SPACE + "toÁst "; + String input2 = " " + NO_BREAK_SPACE + "t" + omicron + "ast " + ZERO_WIDTH_SPACE; + + assertEquals("strings should match", Unicode.sanitize(input1), Unicode.sanitize(input2)); + } + +} diff --git a/src/test/java/org/qortal/test/naming/BuySellTests.java b/src/test/java/org/qortal/test/naming/BuySellTests.java index 23022613..f0320da5 100644 --- a/src/test/java/org/qortal/test/naming/BuySellTests.java +++ b/src/test/java/org/qortal/test/naming/BuySellTests.java @@ -95,7 +95,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Orphan sell-name @@ -103,7 +103,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Re-process sell-name @@ -111,7 +111,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Orphan sell-name and register-name @@ -133,7 +133,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); } @@ -150,7 +150,7 @@ public class BuySellTests extends Common { // Check name is no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Orphan cancel sell-name @@ -158,7 +158,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); } @@ -177,7 +177,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Orphan buy-name @@ -185,7 +185,7 @@ public class BuySellTests extends Common { // Check name is for sale (not sold) nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", price, nameData.getSalePrice()); // Re-process buy-name @@ -193,7 +193,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(bob.getAddress(), nameData.getOwner()); @@ -202,7 +202,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(alice.getAddress(), nameData.getOwner()); @@ -214,7 +214,7 @@ public class BuySellTests extends Common { // Check name is sold nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price assertEquals(bob.getAddress(), nameData.getOwner()); } @@ -233,7 +233,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); // Orphan sell-name @@ -241,7 +241,7 @@ public class BuySellTests extends Common { // Check name no longer for sale nameData = repository.getNameRepository().fromName(name); - assertFalse(nameData.getIsForSale()); + assertFalse(nameData.isForSale()); // Not concerned about price // Re-process sell-name @@ -249,7 +249,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); // Orphan sell-name and buy-name @@ -257,7 +257,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); // Note: original sale price assertEquals("price incorrect", price, nameData.getSalePrice()); assertEquals(alice.getAddress(), nameData.getOwner()); @@ -273,7 +273,7 @@ public class BuySellTests extends Common { // Check name is for sale nameData = repository.getNameRepository().fromName(name); - assertTrue(nameData.getIsForSale()); + assertTrue(nameData.isForSale()); assertEquals("price incorrect", newPrice, nameData.getSalePrice()); assertEquals(bob.getAddress(), nameData.getOwner()); } diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index f124b2dc..0bb750d0 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -13,7 +13,6 @@ import org.qortal.data.transaction.UpdateNameTransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; -import org.qortal.test.common.BlockUtils; import org.qortal.test.common.Common; import org.qortal.test.common.TransactionUtils; import org.qortal.test.common.transaction.TestTransaction; @@ -32,9 +31,10 @@ public class MiscTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String name = "test-name"; + String name = "initial-name"; + String data = "initial-data"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); List recentNames = repository.getNameRepository().getRecentNames(0L); @@ -44,124 +44,6 @@ public class MiscTests extends Common { } } - @Test - public void testUpdateName() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - // Register-name - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String name = "test-name"; - - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); - TransactionUtils.signAndMint(repository, transactionData, alice); - - String newName = "new-name"; - String newData = ""; - transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); - TransactionUtils.signAndMint(repository, transactionData, alice); - - // Check old name no longer exists - assertFalse(repository.getNameRepository().nameExists(name)); - - // Check new name exists - assertTrue(repository.getNameRepository().nameExists(newName)); - - // orphan and recheck - BlockUtils.orphanLastBlock(repository); - - // Check new name no longer exists - assertFalse(repository.getNameRepository().nameExists(newName)); - - // Check old name exists again - assertTrue(repository.getNameRepository().nameExists(name)); - } - } - - // Test that reverting using previous UPDATE_NAME works as expected - @Test - public void testDoubleUpdateName() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - // Register-name - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String name = "test-name"; - - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); - TransactionUtils.signAndMint(repository, transactionData, alice); - - String newName = "new-name"; - String newData = ""; - transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); - TransactionUtils.signAndMint(repository, transactionData, alice); - - // Check old name no longer exists - assertFalse(repository.getNameRepository().nameExists(name)); - - // Check new name exists - assertTrue(repository.getNameRepository().nameExists(newName)); - - String newestName = "newest-name"; - String newestData = "abc"; - transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, newestName, newestData); - TransactionUtils.signAndMint(repository, transactionData, alice); - - // Check previous name no longer exists - assertFalse(repository.getNameRepository().nameExists(newName)); - - // Check newest name exists - assertTrue(repository.getNameRepository().nameExists(newestName)); - - // orphan and recheck - BlockUtils.orphanLastBlock(repository); - - // Check newest name no longer exists - assertFalse(repository.getNameRepository().nameExists(newestName)); - - // Check previous name exists again - assertTrue(repository.getNameRepository().nameExists(newName)); - - // orphan and recheck - BlockUtils.orphanLastBlock(repository); - - // Check new name no longer exists - assertFalse(repository.getNameRepository().nameExists(newName)); - - // Check original name exists again - assertTrue(repository.getNameRepository().nameExists(name)); - } - } - - @Test - public void testUpdateData() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - // Register-name - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - String name = "test-name"; - String data = "{}"; - - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); - TransactionUtils.signAndMint(repository, transactionData, alice); - - String newName = ""; - String newData = "new-data"; - transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); - TransactionUtils.signAndMint(repository, transactionData, alice); - - // Check name still exists - assertTrue(repository.getNameRepository().nameExists(name)); - - // Check data is correct - assertEquals(newData, repository.getNameRepository().fromName(name).getData()); - - // orphan and recheck - BlockUtils.orphanLastBlock(repository); - - // Check name still exists - assertTrue(repository.getNameRepository().nameExists(name)); - - // Check old data restored - assertEquals(data, repository.getNameRepository().fromName(name).getData()); - } - } - // test trying to register same name twice @Test public void testDuplicateRegisterName() throws DataException { @@ -169,12 +51,14 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; + String data = "{}"; - RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); // duplicate - transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + String duplicateName = "TEST-nÁme"; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data); Transaction transaction = Transaction.fromData(repository, transactionData); transaction.sign(alice); @@ -190,17 +74,20 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; + String data = "{}"; - TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}"); + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); - String newName = "new-name"; - String newData = ""; - transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, newData); + // Register another name that we will later attempt to rename to first name (above) + String otherName = "new-name"; + String otherData = ""; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData); TransactionUtils.signAndMint(repository, transactionData, alice); // we shouldn't be able to update name to existing name - transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), newName, name, newData); + String duplicateName = "TEST-nÁme"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), otherName, duplicateName, otherData); Transaction transaction = Transaction.fromData(repository, transactionData); transaction.sign(alice); diff --git a/src/test/java/org/qortal/test/naming/UpdateTests.java b/src/test/java/org/qortal/test/naming/UpdateTests.java new file mode 100644 index 00000000..ffbf7177 --- /dev/null +++ b/src/test/java/org/qortal/test/naming/UpdateTests.java @@ -0,0 +1,334 @@ +package org.qortal.test.naming; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.UpdateNameTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class UpdateTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String newName = "new-name"; + String newData = ""; + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + // Check updated timestamp is correct + assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check old name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + @Test + public void testUpdateNameSameOwner() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String newName = "Initial-Name"; + String newData = ""; + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(newName)); + + // Check updated timestamp is correct + assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(newName)); + + // Check old name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testDoubleUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, initialTransactionData, alice); + + String middleName = "middle-name"; + String middleData = ""; + TransactionData middleTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, middleTransactionData, alice); + + // Check old name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check new name exists + assertTrue(repository.getNameRepository().nameExists(middleName)); + + String newestName = "newest-name"; + String newestData = "newest-data"; + TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, newestTransactionData, alice); + + // Check previous name no longer exists + assertFalse(repository.getNameRepository().nameExists(middleName)); + + // Check newest name exists + assertTrue(repository.getNameRepository().nameExists(newestName)); + + // Check updated timestamp is correct + assertEquals((Long) newestTransactionData.getTimestamp(), repository.getNameRepository().fromName(newestName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check newest name no longer exists + assertFalse(repository.getNameRepository().nameExists(newestName)); + + // Check previous name exists again + assertTrue(repository.getNameRepository().nameExists(middleName)); + + // Check updated timestamp is correct + assertEquals((Long) middleTransactionData.getTimestamp(), repository.getNameRepository().fromName(middleName).getUpdated()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check new name no longer exists + assertFalse(repository.getNameRepository().nameExists(middleName)); + + // Check original name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check updated timestamp is empty + assertNull(repository.getNameRepository().fromName(initialName).getUpdated()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testIntermediateUpdateName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Don't update name, but update data. + // This tests whether reverting a future update/sale can find the correct previous name + String middleName = ""; + String middleData = "middle-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check old name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check previous name no longer exists + assertFalse(repository.getNameRepository().nameExists(initialName)); + + // Check newest name exists + assertTrue(repository.getNameRepository().nameExists(newestName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check original name exists again + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check original name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + } + } + + @Test + public void testUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + String newName = ""; + String newData = "new-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check data is correct + assertEquals(newData, repository.getNameRepository().fromName(initialName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check name still exists + assertTrue(repository.getNameRepository().nameExists(initialName)); + + // Check old data restored + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testDoubleUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Update data + String middleName = "middle-name"; + String middleData = "middle-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData()); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + + // Test that reverting using previous UPDATE_NAME works as expected + @Test + public void testIntermediateUpdateData() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "initial-name"; + String initialData = "initial-data"; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Don't update data, but update name. + // This tests whether reverting a future update/sale can find the correct previous data + String middleName = "middle-name"; + String middleData = ""; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData()); + + String newestName = "newest-name"; + String newestData = "newest-data"; + transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Check data is correct + assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData()); + + // orphan and recheck + BlockUtils.orphanLastBlock(repository); + + // Check data is correct + assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData()); + } + } + +} From 7447ab20a99cde8f2c270cb908d731a8c95618b3 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 15 May 2020 14:18:51 +0100 Subject: [PATCH 4/4] Add index for finding Registered Names using 'reduced' form --- .../org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 22f828f5..e4d7f81a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -292,6 +292,8 @@ public class HSQLDBDatabaseUpdates { + "PRIMARY KEY (name))"); // For finding names by owner stmt.execute("CREATE INDEX NamesOwnerIndex ON Names (owner)"); + // For finding names by 'reduced' form + stmt.execute("CREATE INDEX NamesReducedNameIndex ON Names (reduced_name)"); // Register Name Transactions stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, "