From 197c742ce7a8b63d25415f1c0a68aeb93f25837d Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 15 May 2020 14:08:46 +0100 Subject: [PATCH] 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()); + } + } + +}