diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 11aab89c..1a7b48fe 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1133,7 +1133,7 @@ public class Block { // Check transaction can even be processed validationResult = transaction.isProcessable(); if (validationResult != Transaction.ValidationResult.OK) { - LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); + LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name())); return ValidationResult.TRANSACTION_INVALID; } diff --git a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java index d4455da1..c2b06fd2 100644 --- a/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/RegisterNameTransactionData.java @@ -26,7 +26,7 @@ public class RegisterNameTransactionData extends TransactionData { @Schema(description = "requested name", example = "my-name") private String name; - @Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }") + @Schema(description = "simple name-related info in JSON or text format", example = "Registered Name on the Qortal Chain") private String data; // For internal use diff --git a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java index 43c8da59..b43361db 100644 --- a/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/UpdateNameTransactionData.java @@ -26,7 +26,7 @@ public class UpdateNameTransactionData extends TransactionData { @Schema(description = "new name", example = "my-new-name") private String newName; - @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }") + @Schema(description = "replacement simple name-related info in JSON or text format", example = "Registered Name on the Qortal Chain") private String newData; // For internal use diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 66c1fc8b..ad20ef1c 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -2,11 +2,13 @@ package org.qortal.transaction; import java.util.Collections; import java.util.List; +import java.util.Objects; 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.RegisterNameTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.naming.Name; @@ -77,14 +79,32 @@ public class RegisterNameTransaction extends Transaction { @Override public ValidationResult isProcessable() throws DataException { // Check the name isn't already taken - if (this.repository.getNameRepository().reducedNameExists(this.registerNameTransactionData.getReducedName())) + if (this.repository.getNameRepository().reducedNameExists(this.registerNameTransactionData.getReducedName())) { + // Name exists, but we'll allow the transaction if it has the same creator + // This is necessary to workaround an issue due to inconsistent data in the Names table on some nodes. + // Without this, the chain can get stuck for a subset of nodes when the name is registered + // for the second time. It's simplest to just treat REGISTER_NAME as UPDATE_NAME if the creator + // matches that of the original registration. + + NameData nameData = this.repository.getNameRepository().fromReducedName(this.registerNameTransactionData.getReducedName()); + if (Objects.equals(this.getCreator().getAddress(), nameData.getOwner())) { + // Transaction creator already owns the name, so it's safe to update it + // Treat this as valid, which also requires skipping the "one name per account" check below. + // Given that the name matches one already registered, we know that it won't exceed the limit. + return ValidationResult.OK; + } + + // Name is already registered to someone else return ValidationResult.NAME_ALREADY_REGISTERED; + } // If accounts are only allowed one registered name then check for this if (BlockChain.getInstance().oneNamePerAccount() && !this.repository.getNameRepository().getNamesByOwner(getRegistrant().getAddress()).isEmpty()) return ValidationResult.MULTIPLE_NAMES_FORBIDDEN; + // FUTURE: when adding more validation, make sure to check the `return ValidationResult.OK` above + return ValidationResult.OK; } diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java index c46cbfab..625130e1 100644 --- a/src/test/java/org/qortal/test/naming/MiscTests.java +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -13,6 +13,7 @@ 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,7 +33,7 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "initial-name"; - String data = "initial-data"; + String data = "{\"age\":30}"; RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -44,14 +45,14 @@ public class MiscTests extends Common { } } - // test trying to register same name twice + // test trying to register same name twice (with same creator) @Test - public void testDuplicateRegisterName() throws DataException { + public void testDuplicateRegisterNameWithSameCreator() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - String data = "{}"; + String data = "{\"age\":30}"; RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -63,7 +64,31 @@ public class MiscTests extends Common { transaction.sign(alice); ValidationResult result = transaction.importAsUnconfirmed(); - assertTrue("Transaction should be invalid", ValidationResult.OK != result); + assertTrue("Transaction should be valid because it has the same creator", ValidationResult.OK == result); + } + } + + // test trying to register same name twice (with different creator) + @Test + public void testDuplicateRegisterNameWithDifferentCreator() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{}"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // duplicate (this time registered by Bob) + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + String duplicateName = "TEST-nÁme"; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), duplicateName, data); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid because it has a different creator", ValidationResult.OK != result); } } @@ -74,7 +99,7 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - String data = "{}"; + String data = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -103,7 +128,7 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = alice.getAddress(); - String data = "{}"; + String data = "{\"age\":30}"; RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); Transaction transaction = Transaction.fromData(repository, transactionData); @@ -121,7 +146,7 @@ public class MiscTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String name = "test-name"; - String data = "{}"; + String data = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -138,4 +163,61 @@ public class MiscTests extends Common { } } + // test registering and then orphaning + @Test + public void testRegisterNameAndOrphan() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Register the name + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Ensure the name exists and the data is correct + assertEquals(data, repository.getNameRepository().fromName(name).getData()); + + // Orphan the latest block + BlockUtils.orphanBlocks(repository, 1); + + // Ensure the name doesn't exist once again + assertNull(repository.getNameRepository().fromName(name)); + } + } + + // test registering and then orphaning multiple times (to simulate several re-orgs) + @Test + public void testMultipleRegisterNameAndOrphan() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + for (int i = 0; i < 10; i++) { + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Register the name + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Ensure the name exists and the data is correct + assertEquals(data, repository.getNameRepository().fromName(name).getData()); + + // Orphan the latest block + BlockUtils.orphanBlocks(repository, 1); + + // Ensure the name doesn't exist once again + assertNull(repository.getNameRepository().fromName(name)); + } + } + } + } diff --git a/src/test/java/org/qortal/test/naming/UpdateTests.java b/src/test/java/org/qortal/test/naming/UpdateTests.java index ffbf7177..134d3358 100644 --- a/src/test/java/org/qortal/test/naming/UpdateTests.java +++ b/src/test/java/org/qortal/test/naming/UpdateTests.java @@ -29,7 +29,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, initialTransactionData, alice); @@ -68,7 +68,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, initialTransactionData, alice); @@ -108,7 +108,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, initialTransactionData, alice); @@ -171,7 +171,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -217,7 +217,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -251,7 +251,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, transactionData, alice); @@ -294,7 +294,7 @@ public class UpdateTests extends Common { // Register-name PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); String initialName = "initial-name"; - String initialData = "initial-data"; + String initialData = "{\"age\":30}"; TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData); TransactionUtils.signAndMint(repository, transactionData, alice);