diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 1a7b48fe..af5c6b01 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -41,12 +41,14 @@ import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; import org.qortal.data.block.BlockTransactionData; import org.qortal.data.network.OnlineAccountData; +import org.qortal.data.transaction.RegisterNameTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.ATRepository; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.TransactionRepository; import org.qortal.transaction.AtTransaction; +import org.qortal.transaction.RegisterNameTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.TransactionType; @@ -1282,6 +1284,21 @@ public class Block { return mintingAccount.canMint(); } + /** + * Pre-process block, and its transactions. + * This allows for any database integrity checks prior to validation. + * This is called before isValid() and process() + * + * @throws DataException + */ + public void preProcess() throws DataException { + List blocksTransactions = this.getTransactions(); + + for (Transaction transaction : blocksTransactions) { + transaction.preProcess(); + } + } + /** * Process block, and its transactions, adding them to the blockchain. * diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 67a202df..0cf33f43 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -249,6 +249,8 @@ public class BlockMinter extends Thread { if (testBlock.isTimestampValid() != ValidationResult.OK) continue; + testBlock.preProcess(); + // Is new block valid yet? (Before adding unconfirmed transactions) ValidationResult result = testBlock.isValid(); if (result != ValidationResult.OK) { diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 975873da..f9e681ab 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -429,8 +429,9 @@ public class Controller extends Thread { return; // Not System.exit() so that GUI can display error } - // Check database integrity + // Rebuild Names table and check database integrity NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildAllNames(); namesDatabaseIntegrityCheck.runIntegrityCheck(); LOGGER.info("Validating blockchain"); diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 2487a1f7..b5bce3c5 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -1064,6 +1064,8 @@ public class Synchronizer { if (Controller.isStopping()) return SynchronizationResult.SHUTTING_DOWN; + newBlock.preProcess(); + ValidationResult blockResult = newBlock.isValid(); if (blockResult != ValidationResult.OK) { LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, @@ -1157,6 +1159,8 @@ public class Synchronizer { for (Transaction transaction : newBlock.getTransactions()) transaction.setInitialApprovalStatus(); + newBlock.preProcess(); + ValidationResult blockResult = newBlock.isValid(); if (blockResult != ValidationResult.OK) { LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, diff --git a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java index 3760f032..f12bd14a 100644 --- a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java +++ b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java @@ -5,16 +5,13 @@ import org.apache.logging.log4j.Logger; import org.qortal.account.PublicKeyAccount; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.data.naming.NameData; -import org.qortal.data.transaction.BuyNameTransactionData; -import org.qortal.data.transaction.RegisterNameTransactionData; -import org.qortal.data.transaction.TransactionData; -import org.qortal.data.transaction.UpdateNameTransactionData; +import org.qortal.data.transaction.*; import org.qortal.naming.Name; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.transaction.Transaction.TransactionType; -import org.qortal.utils.Base58; +import org.qortal.utils.Unicode; import java.util.*; @@ -22,31 +19,127 @@ public class NamesDatabaseIntegrityCheck { private static final Logger LOGGER = LogManager.getLogger(NamesDatabaseIntegrityCheck.class); - private static final List REGISTER_NAME_TX_TYPE = Collections.singletonList(TransactionType.REGISTER_NAME); - private static final List UPDATE_NAME_TX_TYPE = Collections.singletonList(TransactionType.UPDATE_NAME); - private static final List BUY_NAME_TX_TYPE = Collections.singletonList(TransactionType.BUY_NAME); + private static final List ALL_NAME_TX_TYPE = Arrays.asList( + TransactionType.REGISTER_NAME, + TransactionType.UPDATE_NAME, + TransactionType.BUY_NAME, + TransactionType.SELL_NAME + ); - private List registerNameTransactions; - private List updateNameTransactions; - private List buyNameTransactions; + private List nameTransactions = new ArrayList<>(); + + public int rebuildName(String name, Repository repository) { + int modificationCount = 0; + try { + List transactions = this.fetchAllTransactionsInvolvingName(name, repository); + if (transactions.isEmpty()) { + // This name was never registered, so there's nothing to do + return modificationCount; + } + + // Loop through each past transaction and re-apply it to the Names table + for (TransactionData currentTransaction : transactions) { + + // Process REGISTER_NAME transactions + if (currentTransaction.getType() == TransactionType.REGISTER_NAME) { + RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) currentTransaction; + Name nameObj = new Name(repository, registerNameTransactionData); + nameObj.register(); + modificationCount++; + LOGGER.trace("Processed REGISTER_NAME transaction for name {}", name); + } + + // Process UPDATE_NAME transactions + if (currentTransaction.getType() == TransactionType.UPDATE_NAME) { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) currentTransaction; + + if (Objects.equals(updateNameTransactionData.getNewName(), name) && + !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) { + // This renames an existing name, so we need to process that instead + this.rebuildName(updateNameTransactionData.getName(), repository); + } + else { + Name nameObj = new Name(repository, name); + if (nameObj != null && nameObj.getNameData() != null) { + nameObj.update(updateNameTransactionData); + modificationCount++; + LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name); + } else { + // Something went wrong + throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName())); + } + } + } + + // Process SELL_NAME transactions + if (currentTransaction.getType() == TransactionType.SELL_NAME) { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) currentTransaction; + Name nameObj = new Name(repository, sellNameTransactionData.getName()); + if (nameObj != null && nameObj.getNameData() != null) { + nameObj.sell(sellNameTransactionData); + modificationCount++; + LOGGER.trace("Processed SELL_NAME transaction for name {}", name); + } + else { + // Something went wrong + throw new DataException(String.format("Name data not found for name %s", sellNameTransactionData.getName())); + } + } + + // Process BUY_NAME transactions + if (currentTransaction.getType() == TransactionType.BUY_NAME) { + BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction; + Name nameObj = new Name(repository, buyNameTransactionData.getName()); + if (nameObj != null && nameObj.getNameData() != null) { + nameObj.buy(buyNameTransactionData); + modificationCount++; + LOGGER.trace("Processed BUY_NAME transaction for name {}", name); + } + else { + // Something went wrong + throw new DataException(String.format("Name data not found for name %s", buyNameTransactionData.getName())); + } + } + } + + } catch (DataException e) { + LOGGER.info("Unable to run integrity check for name {}: {}", name, e.getMessage()); + } + + return modificationCount; + } + + public int rebuildAllNames() { + int modificationCount = 0; + try (final Repository repository = RepositoryManager.getRepository()) { + List names = this.fetchAllNames(repository); + for (String name : names) { + modificationCount += this.rebuildName(name, repository); + } + repository.saveChanges(); + } + catch (DataException e) { + LOGGER.info("Error when running integrity check for all names: {}", e.getMessage()); + } + + //LOGGER.info("modificationCount: {}", modificationCount); + return modificationCount; + } public void runIntegrityCheck() { boolean integrityCheckFailed = false; - boolean corrected = false; try (final Repository repository = RepositoryManager.getRepository()) { - // Fetch all the (confirmed) name-related transactions - this.fetchRegisterNameTransactions(repository); - this.fetchUpdateNameTransactions(repository); - this.fetchBuyNameTransactions(repository); + // Fetch all the (confirmed) REGISTER_NAME transactions + List registerNameTransactions = this.fetchRegisterNameTransactions(); // Loop through each REGISTER_NAME txn signature and request the full transaction data - for (RegisterNameTransactionData registerNameTransactionData : this.registerNameTransactions) { + for (RegisterNameTransactionData registerNameTransactionData : registerNameTransactions) { String registeredName = registerNameTransactionData.getName(); NameData nameData = repository.getNameRepository().fromName(registeredName); // Check to see if this name has been updated or bought at any point - TransactionData latestUpdate = this.fetchLatestModificationTransactionInvolvingName(registeredName); + TransactionData latestUpdate = this.fetchLatestModificationTransactionInvolvingName(registeredName, repository); if (latestUpdate == null) { // Name was never updated once registered // We expect this name to still be registered to this transaction's creator @@ -54,16 +147,9 @@ public class NamesDatabaseIntegrityCheck { if (nameData == null) { LOGGER.info("Error: registered name {} doesn't exist in Names table. Adding...", registeredName); integrityCheckFailed = true; - - // Register the name - Name name = new Name(repository, registerNameTransactionData); - name.register(); - repository.saveChanges(); - corrected = true; - continue; } else { - //LOGGER.info("Registered name {} is correctly registered", registeredName); + LOGGER.trace("Registered name {} is correctly registered", registeredName); } // Check the owner is correct @@ -72,18 +158,16 @@ public class NamesDatabaseIntegrityCheck { LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", registeredName, nameData.getOwner(), creator.getAddress()); integrityCheckFailed = true; - - // FUTURE: Fix the name's owner if we ever see the above log entry } else { - //LOGGER.info("Registered name {} has the correct owner", registeredName); + LOGGER.trace("Registered name {} has the correct owner", registeredName); } } else { // Check if owner is correct after update // Check for name updates - if (latestUpdate instanceof UpdateNameTransactionData) { + if (latestUpdate.getType() == TransactionType.UPDATE_NAME) { UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) latestUpdate; PublicKeyAccount creator = new PublicKeyAccount(repository, updateNameTransactionData.getCreatorPublicKey()); @@ -93,10 +177,9 @@ public class NamesDatabaseIntegrityCheck { LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", registeredName, nameData.getOwner(), creator.getAddress()); integrityCheckFailed = true; - - // FUTURE: Fix the name's owner if we ever see the above log entry - } else { - //LOGGER.info("Registered name {} has the correct owner after being updated", registeredName); + } + else { + LOGGER.trace("Registered name {} has the correct owner after being updated", registeredName); } } @@ -109,10 +192,9 @@ public class NamesDatabaseIntegrityCheck { LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", updateNameTransactionData.getNewName(), newNameData.getOwner(), creator.getAddress()); integrityCheckFailed = true; - - // FUTURE: Fix the name's owner if we ever see the above log entry - } else { - //LOGGER.info("Registered name {} has the correct owner after being updated", updateNameTransactionData.getNewName()); + } + else { + LOGGER.trace("Registered name {} has the correct owner after being updated", updateNameTransactionData.getNewName()); } } @@ -121,18 +203,31 @@ public class NamesDatabaseIntegrityCheck { } } - // Check for name sales - else if (latestUpdate instanceof BuyNameTransactionData) { + // Check for name buys + else if (latestUpdate.getType() == TransactionType.BUY_NAME) { BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) latestUpdate; PublicKeyAccount creator = new PublicKeyAccount(repository, buyNameTransactionData.getCreatorPublicKey()); if (!Objects.equals(creator.getAddress(), nameData.getOwner())) { LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", registeredName, nameData.getOwner(), creator.getAddress()); integrityCheckFailed = true; + } + else { + LOGGER.trace("Registered name {} has the correct owner after being bought", registeredName); + } + } - // FUTURE: Fix the name's owner if we ever see the above log entry - } else { - //LOGGER.info("Registered name {} has the correct owner after being bought", registeredName); + // Check for name sells + else if (latestUpdate.getType() == TransactionType.SELL_NAME) { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) latestUpdate; + PublicKeyAccount creator = new PublicKeyAccount(repository, sellNameTransactionData.getCreatorPublicKey()); + if (!Objects.equals(creator.getAddress(), nameData.getOwner())) { + LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", + registeredName, nameData.getOwner(), creator.getAddress()); + integrityCheckFailed = true; + } + else { + LOGGER.trace("Registered name {} has the correct owner after being listed for sale", registeredName); } } @@ -150,147 +245,166 @@ public class NamesDatabaseIntegrityCheck { } if (integrityCheckFailed) { - if (corrected) { - LOGGER.info("Registered names database integrity check failed, but corrections were made. If this " + - "problem persists after restarting the node, you may need to switch to a recent bootstrap."); - } - else { - LOGGER.info("Registered names database integrity check failed. Bootstrapping is recommended."); - } + LOGGER.info("Registered names database integrity check failed. Bootstrapping is recommended."); } else { LOGGER.info("Registered names database integrity check passed."); } } - private void fetchRegisterNameTransactions(Repository repository) throws DataException { + private List fetchRegisterNameTransactions() { List registerNameTransactions = new ArrayList<>(); - // Fetch all the confirmed REGISTER_NAME transaction signatures - List registerNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( - null, null, null, REGISTER_NAME_TX_TYPE, null, null, - ConfirmationStatus.CONFIRMED, null, null, false); - - for (byte[] signature : registerNameSigs) { - // LOGGER.info("Fetching REGISTER_NAME transaction from signature {}...", Base58.encode(signature)); - - TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); - if (!(transactionData instanceof RegisterNameTransactionData)) { - LOGGER.info("REGISTER_NAME transaction signature {} not found", Base58.encode(signature)); - continue; + for (TransactionData transactionData : this.nameTransactions) { + if (transactionData.getType() == TransactionType.REGISTER_NAME) { + RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; + registerNameTransactions.add(registerNameTransactionData); } - RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; - registerNameTransactions.add(registerNameTransactionData); } - this.registerNameTransactions = registerNameTransactions; + return registerNameTransactions; } - private void fetchUpdateNameTransactions(Repository repository) throws DataException { + private List fetchUpdateNameTransactions() { List updateNameTransactions = new ArrayList<>(); - // Fetch all the confirmed REGISTER_NAME transaction signatures - List updateNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( - null, null, null, UPDATE_NAME_TX_TYPE, null, null, - ConfirmationStatus.CONFIRMED, null, null, false); - - for (byte[] signature : updateNameSigs) { - // LOGGER.info("Fetching UPDATE_NAME transaction from signature {}...", Base58.encode(signature)); - - TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); - if (!(transactionData instanceof UpdateNameTransactionData)) { - LOGGER.info("UPDATE_NAME transaction signature {} not found", Base58.encode(signature)); - continue; + for (TransactionData transactionData : this.nameTransactions) { + if (transactionData.getType() == TransactionType.UPDATE_NAME) { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; + updateNameTransactions.add(updateNameTransactionData); } - UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; - updateNameTransactions.add(updateNameTransactionData); } - this.updateNameTransactions = updateNameTransactions; + return updateNameTransactions; } - private void fetchBuyNameTransactions(Repository repository) throws DataException { + private List fetchSellNameTransactions() { + List sellNameTransactions = new ArrayList<>(); + + for (TransactionData transactionData : this.nameTransactions) { + if (transactionData.getType() == TransactionType.SELL_NAME) { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; + sellNameTransactions.add(sellNameTransactionData); + } + } + return sellNameTransactions; + } + + private List fetchBuyNameTransactions() { List buyNameTransactions = new ArrayList<>(); + for (TransactionData transactionData : this.nameTransactions) { + if (transactionData.getType() == TransactionType.BUY_NAME) { + BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData; + buyNameTransactions.add(buyNameTransactionData); + } + } + return buyNameTransactions; + } + + private void fetchAllNameTransactions(Repository repository) throws DataException { + List nameTransactions = new ArrayList<>(); + // Fetch all the confirmed REGISTER_NAME transaction signatures - List buyNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( - null, null, null, BUY_NAME_TX_TYPE, null, null, + List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria( + null, null, null, ALL_NAME_TX_TYPE, null, null, ConfirmationStatus.CONFIRMED, null, null, false); - for (byte[] signature : buyNameSigs) { - // LOGGER.info("Fetching BUY_NAME transaction from signature {}...", Base58.encode(signature)); - + for (byte[] signature : signatures) { TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); - if (!(transactionData instanceof BuyNameTransactionData)) { - LOGGER.info("BUY_NAME transaction signature {} not found", Base58.encode(signature)); - continue; - } - BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData; - buyNameTransactions.add(buyNameTransactionData); + nameTransactions.add(transactionData); } - this.buyNameTransactions = buyNameTransactions; + this.nameTransactions = nameTransactions; } - private List fetchUpdateTransactionsInvolvingName(String registeredName) { - List matchedTransactions = new ArrayList<>(); + private List fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException { + List transactions = new ArrayList<>(); + String reducedName = Unicode.sanitize(name); - for (UpdateNameTransactionData updateNameTransactionData : this.updateNameTransactions) { - if (Objects.equals(updateNameTransactionData.getName(), registeredName) || - Objects.equals(updateNameTransactionData.getNewName(), registeredName)) { + // Fetch all the confirmed name-modification transactions + if (this.nameTransactions.isEmpty()) { + this.fetchAllNameTransactions(repository); + } - matchedTransactions.add(updateNameTransactionData); + for (TransactionData transactionData : this.nameTransactions) { + + if ((transactionData instanceof RegisterNameTransactionData)) { + RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; + if (Objects.equals(registerNameTransactionData.getReducedName(), reducedName)) { + transactions.add(transactionData); + } + } + if ((transactionData instanceof UpdateNameTransactionData)) { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; + if (Objects.equals(updateNameTransactionData.getName(), name) || + Objects.equals(updateNameTransactionData.getReducedNewName(), reducedName)) { + transactions.add(transactionData); + } + } + if ((transactionData instanceof BuyNameTransactionData)) { + BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData; + if (Objects.equals(buyNameTransactionData.getName(), name)) { + transactions.add(transactionData); + } + } + if ((transactionData instanceof SellNameTransactionData)) { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; + if (Objects.equals(sellNameTransactionData.getName(), name)) { + transactions.add(transactionData); + } } } - return matchedTransactions; + return transactions; } - private List fetchBuyTransactionsInvolvingName(String registeredName) { - List matchedTransactions = new ArrayList<>(); + private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName, Repository repository) throws DataException { + List transactionsInvolvingName = this.fetchAllTransactionsInvolvingName(registeredName, repository); - for (BuyNameTransactionData buyNameTransactionData : this.buyNameTransactions) { - if (Objects.equals(buyNameTransactionData.getName(), registeredName)) { - - matchedTransactions.add(buyNameTransactionData); - } - } - return matchedTransactions; - } - - private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName) { - List latestTransactions = new ArrayList<>(); - - List updates = this.fetchUpdateTransactionsInvolvingName(registeredName); - List buys = this.fetchBuyTransactionsInvolvingName(registeredName); - - // Get the latest updates for this name - UpdateNameTransactionData latestUpdateToName = updates.stream() - .filter(update -> update.getNewName().equals(registeredName)) - .max(Comparator.comparing(UpdateNameTransactionData::getTimestamp)) - .orElse(null); - if (latestUpdateToName != null) { - latestTransactions.add(latestUpdateToName); - } - - UpdateNameTransactionData latestUpdateFromName = updates.stream() - .filter(update -> update.getName().equals(registeredName)) - .max(Comparator.comparing(UpdateNameTransactionData::getTimestamp)) - .orElse(null); - if (latestUpdateFromName != null) { - latestTransactions.add(latestUpdateFromName); - } - - // Get the latest buy for this name - BuyNameTransactionData latestBuyForName = buys.stream() - .filter(update -> update.getName().equals(registeredName)) - .max(Comparator.comparing(BuyNameTransactionData::getTimestamp)) - .orElse(null); - if (latestBuyForName != null) { - latestTransactions.add(latestBuyForName); - } - - // Get the latest name-related transaction of any type - TransactionData latestUpdate = latestTransactions.stream() + // Get the latest update for this name (excluding REGISTER_NAME transactions) + TransactionData latestUpdateToName = transactionsInvolvingName.stream() + .filter(txn -> txn.getType() != TransactionType.REGISTER_NAME) .max(Comparator.comparing(TransactionData::getTimestamp)) .orElse(null); - return latestUpdate; + return latestUpdateToName; + } + + private List fetchAllNames(Repository repository) throws DataException { + List names = new ArrayList<>(); + + // Fetch all the confirmed name transactions + if (this.nameTransactions.isEmpty()) { + this.fetchAllNameTransactions(repository); + } + + for (TransactionData transactionData : this.nameTransactions) { + + if ((transactionData instanceof RegisterNameTransactionData)) { + RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; + if (!names.contains(registerNameTransactionData.getName())) { + names.add(registerNameTransactionData.getName()); + } + } + if ((transactionData instanceof UpdateNameTransactionData)) { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; + if (!names.contains(updateNameTransactionData.getName())) { + names.add(updateNameTransactionData.getName()); + } + if (!names.contains(updateNameTransactionData.getNewName())) { + names.add(updateNameTransactionData.getNewName()); + } + } + if ((transactionData instanceof BuyNameTransactionData)) { + BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData; + if (!names.contains(buyNameTransactionData.getName())) { + names.add(buyNameTransactionData.getName()); + } + } + if ((transactionData instanceof SellNameTransactionData)) { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; + if (!names.contains(sellNameTransactionData.getName())) { + names.add(sellNameTransactionData.getName()); + } + } + } + return names; } } diff --git a/src/main/java/org/qortal/naming/Name.java b/src/main/java/org/qortal/naming/Name.java index 454ade57..b27e9454 100644 --- a/src/main/java/org/qortal/naming/Name.java +++ b/src/main/java/org/qortal/naming/Name.java @@ -265,4 +265,8 @@ public class Name { return previousTransactionData.getTimestamp(); } + public NameData getNameData() { + return this.nameData; + } + } diff --git a/src/main/java/org/qortal/transaction/AccountFlagsTransaction.java b/src/main/java/org/qortal/transaction/AccountFlagsTransaction.java index 355340b6..4362b1a9 100644 --- a/src/main/java/org/qortal/transaction/AccountFlagsTransaction.java +++ b/src/main/java/org/qortal/transaction/AccountFlagsTransaction.java @@ -48,6 +48,11 @@ public class AccountFlagsTransaction extends Transaction { return ValidationResult.NO_FLAG_PERMISSION; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { Account target = this.getTarget(); diff --git a/src/main/java/org/qortal/transaction/AccountLevelTransaction.java b/src/main/java/org/qortal/transaction/AccountLevelTransaction.java index da986344..18324c34 100644 --- a/src/main/java/org/qortal/transaction/AccountLevelTransaction.java +++ b/src/main/java/org/qortal/transaction/AccountLevelTransaction.java @@ -49,6 +49,11 @@ public class AccountLevelTransaction extends Transaction { return ValidationResult.NO_FLAG_PERMISSION; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { Account target = getTarget(); diff --git a/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java b/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java index d62bd451..15dc51bf 100644 --- a/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java +++ b/src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java @@ -84,6 +84,11 @@ public class AddGroupAdminTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group adminship @@ -98,4 +103,4 @@ public class AddGroupAdminTransaction extends Transaction { group.unpromoteToAdmin(this.addGroupAdminTransactionData); } -} \ No newline at end of file +} diff --git a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java index 04ecc09f..f75b7f19 100644 --- a/src/main/java/org/qortal/transaction/ArbitraryTransaction.java +++ b/src/main/java/org/qortal/transaction/ArbitraryTransaction.java @@ -60,6 +60,11 @@ public class ArbitraryTransaction extends Transaction { arbitraryTransactionData.getFee()); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Wrap and delegate payment processing to Payment class. diff --git a/src/main/java/org/qortal/transaction/AtTransaction.java b/src/main/java/org/qortal/transaction/AtTransaction.java index a7e72b2a..c570bb65 100644 --- a/src/main/java/org/qortal/transaction/AtTransaction.java +++ b/src/main/java/org/qortal/transaction/AtTransaction.java @@ -80,6 +80,11 @@ public class AtTransaction extends Transaction { return Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference()); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public ValidationResult isValid() throws DataException { // Check recipient address is valid diff --git a/src/main/java/org/qortal/transaction/BuyNameTransaction.java b/src/main/java/org/qortal/transaction/BuyNameTransaction.java index ad3e0c8d..c4e5f29c 100644 --- a/src/main/java/org/qortal/transaction/BuyNameTransaction.java +++ b/src/main/java/org/qortal/transaction/BuyNameTransaction.java @@ -6,6 +6,7 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.block.BlockChain; +import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; @@ -98,6 +99,17 @@ public class BuyNameTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData; + + // Rebuild this name in the Names table from the transaction history + // This is necessary because in some rare cases names can be missing from the Names table after registration + // but we have been unable to reproduce the issue and track down the root cause + NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildName(buyNameTransactionData.getName(), this.repository); + } + @Override public void process() throws DataException { // Buy Name diff --git a/src/main/java/org/qortal/transaction/CancelAssetOrderTransaction.java b/src/main/java/org/qortal/transaction/CancelAssetOrderTransaction.java index b8b70dde..955f62f4 100644 --- a/src/main/java/org/qortal/transaction/CancelAssetOrderTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelAssetOrderTransaction.java @@ -62,6 +62,11 @@ public class CancelAssetOrderTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Mark Order as completed so no more trades can happen diff --git a/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java b/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java index e01be7be..483dfc6f 100644 --- a/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java @@ -83,6 +83,11 @@ public class CancelGroupBanTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java b/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java index ea228215..800f2444 100644 --- a/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java @@ -83,6 +83,11 @@ public class CancelGroupInviteTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java index f241db47..788492a9 100644 --- a/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelSellNameTransaction.java @@ -79,6 +79,11 @@ public class CancelSellNameTransaction extends Transaction { } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Name diff --git a/src/main/java/org/qortal/transaction/ChatTransaction.java b/src/main/java/org/qortal/transaction/ChatTransaction.java index a670ea4b..2202d44a 100644 --- a/src/main/java/org/qortal/transaction/ChatTransaction.java +++ b/src/main/java/org/qortal/transaction/ChatTransaction.java @@ -135,6 +135,11 @@ public class ChatTransaction extends Transaction { return true; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public ValidationResult isValid() throws DataException { // Nonce checking is done via isSignatureValid() as that method is only called once per import diff --git a/src/main/java/org/qortal/transaction/CreateAssetOrderTransaction.java b/src/main/java/org/qortal/transaction/CreateAssetOrderTransaction.java index 36cccf42..24e57a4e 100644 --- a/src/main/java/org/qortal/transaction/CreateAssetOrderTransaction.java +++ b/src/main/java/org/qortal/transaction/CreateAssetOrderTransaction.java @@ -135,6 +135,11 @@ public class CreateAssetOrderTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Order Id is transaction's signature diff --git a/src/main/java/org/qortal/transaction/CreateGroupTransaction.java b/src/main/java/org/qortal/transaction/CreateGroupTransaction.java index 7ed61684..6f4a3634 100644 --- a/src/main/java/org/qortal/transaction/CreateGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/CreateGroupTransaction.java @@ -92,6 +92,11 @@ public class CreateGroupTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Create Group diff --git a/src/main/java/org/qortal/transaction/CreatePollTransaction.java b/src/main/java/org/qortal/transaction/CreatePollTransaction.java index 4c4b3a0a..a56322a7 100644 --- a/src/main/java/org/qortal/transaction/CreatePollTransaction.java +++ b/src/main/java/org/qortal/transaction/CreatePollTransaction.java @@ -106,6 +106,11 @@ public class CreatePollTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Publish poll to allow voting diff --git a/src/main/java/org/qortal/transaction/DeployAtTransaction.java b/src/main/java/org/qortal/transaction/DeployAtTransaction.java index 86e04d56..f3024b57 100644 --- a/src/main/java/org/qortal/transaction/DeployAtTransaction.java +++ b/src/main/java/org/qortal/transaction/DeployAtTransaction.java @@ -203,6 +203,11 @@ public class DeployAtTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { ensureATAddress(this.deployAtTransactionData); diff --git a/src/main/java/org/qortal/transaction/GenesisTransaction.java b/src/main/java/org/qortal/transaction/GenesisTransaction.java index 067ff183..74a84a7d 100644 --- a/src/main/java/org/qortal/transaction/GenesisTransaction.java +++ b/src/main/java/org/qortal/transaction/GenesisTransaction.java @@ -100,6 +100,11 @@ public class GenesisTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { Account recipient = new Account(repository, this.genesisTransactionData.getRecipient()); diff --git a/src/main/java/org/qortal/transaction/GroupApprovalTransaction.java b/src/main/java/org/qortal/transaction/GroupApprovalTransaction.java index d5cf66f7..1c8bb709 100644 --- a/src/main/java/org/qortal/transaction/GroupApprovalTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupApprovalTransaction.java @@ -66,6 +66,11 @@ public class GroupApprovalTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Find previous approval decision (if any) by this admin for pending transaction diff --git a/src/main/java/org/qortal/transaction/GroupBanTransaction.java b/src/main/java/org/qortal/transaction/GroupBanTransaction.java index d3458ebe..c9a6c307 100644 --- a/src/main/java/org/qortal/transaction/GroupBanTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupBanTransaction.java @@ -87,6 +87,11 @@ public class GroupBanTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/GroupInviteTransaction.java b/src/main/java/org/qortal/transaction/GroupInviteTransaction.java index a66f7584..f3b08f59 100644 --- a/src/main/java/org/qortal/transaction/GroupInviteTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupInviteTransaction.java @@ -88,6 +88,11 @@ public class GroupInviteTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/GroupKickTransaction.java b/src/main/java/org/qortal/transaction/GroupKickTransaction.java index d9be8161..84de3a59 100644 --- a/src/main/java/org/qortal/transaction/GroupKickTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupKickTransaction.java @@ -89,6 +89,11 @@ public class GroupKickTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/IssueAssetTransaction.java b/src/main/java/org/qortal/transaction/IssueAssetTransaction.java index e9422dcd..52428963 100644 --- a/src/main/java/org/qortal/transaction/IssueAssetTransaction.java +++ b/src/main/java/org/qortal/transaction/IssueAssetTransaction.java @@ -92,6 +92,11 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Issue asset diff --git a/src/main/java/org/qortal/transaction/JoinGroupTransaction.java b/src/main/java/org/qortal/transaction/JoinGroupTransaction.java index ed69ed4e..bc62c629 100644 --- a/src/main/java/org/qortal/transaction/JoinGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/JoinGroupTransaction.java @@ -67,6 +67,11 @@ public class JoinGroupTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/LeaveGroupTransaction.java b/src/main/java/org/qortal/transaction/LeaveGroupTransaction.java index ad31e565..1e8f8c6c 100644 --- a/src/main/java/org/qortal/transaction/LeaveGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/LeaveGroupTransaction.java @@ -67,6 +67,11 @@ public class LeaveGroupTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group Membership diff --git a/src/main/java/org/qortal/transaction/MessageTransaction.java b/src/main/java/org/qortal/transaction/MessageTransaction.java index ef6e6c76..d02b6fdd 100644 --- a/src/main/java/org/qortal/transaction/MessageTransaction.java +++ b/src/main/java/org/qortal/transaction/MessageTransaction.java @@ -239,6 +239,11 @@ public class MessageTransaction extends Transaction { getPaymentData(), this.messageTransactionData.getFee(), true); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // If we have no amount then there's nothing to do diff --git a/src/main/java/org/qortal/transaction/MultiPaymentTransaction.java b/src/main/java/org/qortal/transaction/MultiPaymentTransaction.java index 4c3f75dc..34cd0147 100644 --- a/src/main/java/org/qortal/transaction/MultiPaymentTransaction.java +++ b/src/main/java/org/qortal/transaction/MultiPaymentTransaction.java @@ -67,6 +67,11 @@ public class MultiPaymentTransaction extends Transaction { return new Payment(this.repository).isProcessable(this.multiPaymentTransactionData.getSenderPublicKey(), payments, this.multiPaymentTransactionData.getFee()); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Wrap and delegate payment processing to Payment class. diff --git a/src/main/java/org/qortal/transaction/PaymentTransaction.java b/src/main/java/org/qortal/transaction/PaymentTransaction.java index f6caaef5..4869db76 100644 --- a/src/main/java/org/qortal/transaction/PaymentTransaction.java +++ b/src/main/java/org/qortal/transaction/PaymentTransaction.java @@ -61,6 +61,11 @@ public class PaymentTransaction extends Transaction { return new Payment(this.repository).isProcessable(this.paymentTransactionData.getSenderPublicKey(), getPaymentData(), this.paymentTransactionData.getFee()); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Wrap and delegate payment processing to Payment class. diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java index 729270e0..0d28d382 100644 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ b/src/main/java/org/qortal/transaction/PresenceTransaction.java @@ -149,6 +149,11 @@ public class PresenceTransaction extends Transaction { return true; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public ValidationResult isValid() throws DataException { // Nonce checking is done via isSignatureValid() as that method is only called once per import diff --git a/src/main/java/org/qortal/transaction/PublicizeTransaction.java b/src/main/java/org/qortal/transaction/PublicizeTransaction.java index 75cfd2a2..c03c8283 100644 --- a/src/main/java/org/qortal/transaction/PublicizeTransaction.java +++ b/src/main/java/org/qortal/transaction/PublicizeTransaction.java @@ -80,6 +80,11 @@ public class PublicizeTransaction extends Transaction { return true; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public ValidationResult isValid() throws DataException { // There can be only one diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 66c1fc8b..d0a2f49c 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -6,6 +6,7 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.block.BlockChain; +import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.crypto.Crypto; import org.qortal.data.transaction.RegisterNameTransactionData; import org.qortal.data.transaction.TransactionData; @@ -88,6 +89,17 @@ public class RegisterNameTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; + + // Rebuild this name in the Names table from the transaction history + // This is necessary because in some rare cases names can be missing from the Names table after registration + // but we have been unable to reproduce the issue and track down the root cause + NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildName(registerNameTransactionData.getName(), this.repository); + } + @Override public void process() throws DataException { // Register Name diff --git a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java index 43f1fc8f..3e5f1e6d 100644 --- a/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java +++ b/src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java @@ -87,6 +87,11 @@ public class RemoveGroupAdminTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group adminship @@ -107,4 +112,4 @@ public class RemoveGroupAdminTransaction extends Transaction { this.repository.getTransactionRepository().save(this.removeGroupAdminTransactionData); } -} \ No newline at end of file +} diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java index 0e21c0c6..be68196d 100644 --- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java +++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java @@ -159,6 +159,11 @@ public class RewardShareTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { PublicKeyAccount mintingAccount = getMintingAccount(); diff --git a/src/main/java/org/qortal/transaction/SellNameTransaction.java b/src/main/java/org/qortal/transaction/SellNameTransaction.java index 81bd9ff7..c2ab2eb9 100644 --- a/src/main/java/org/qortal/transaction/SellNameTransaction.java +++ b/src/main/java/org/qortal/transaction/SellNameTransaction.java @@ -5,6 +5,7 @@ import java.util.List; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.SellNameTransactionData; import org.qortal.data.transaction.TransactionData; @@ -89,6 +90,17 @@ public class SellNameTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData; + + // Rebuild this name in the Names table from the transaction history + // This is necessary because in some rare cases names can be missing from the Names table after registration + // but we have been unable to reproduce the issue and track down the root cause + NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildName(sellNameTransactionData.getName(), this.repository); + } + @Override public void process() throws DataException { // Sell Name diff --git a/src/main/java/org/qortal/transaction/SetGroupTransaction.java b/src/main/java/org/qortal/transaction/SetGroupTransaction.java index 084044a7..48248b69 100644 --- a/src/main/java/org/qortal/transaction/SetGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/SetGroupTransaction.java @@ -56,6 +56,11 @@ public class SetGroupTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { Account creator = getCreator(); diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 3c761d28..7eb93bc4 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -791,6 +791,8 @@ public abstract class Transaction { // Fix up approval status this.setInitialApprovalStatus(); + this.preProcess(); + ValidationResult validationResult = this.isValidUnconfirmed(); if (validationResult != ValidationResult.OK) return validationResult; @@ -891,6 +893,14 @@ public abstract class Transaction { return ValidationResult.OK; } + /** + * * Pre-process a transaction before validating or processing the block + * This allows for any database integrity checks prior to validation. + * + * @throws DataException + */ + public abstract void preProcess() throws DataException; + /** * Actually process a transaction, updating the blockchain. *

diff --git a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java index a2855a35..79d485a5 100644 --- a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java +++ b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java @@ -61,6 +61,11 @@ public class TransferAssetTransaction extends Transaction { return new Payment(this.repository).isProcessable(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee()); } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Wrap asset transfer as a payment and delegate processing to Payment class. diff --git a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java index d64e953e..f77dac15 100644 --- a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java +++ b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java @@ -68,6 +68,11 @@ public class TransferPrivsTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { Account sender = this.getSender(); diff --git a/src/main/java/org/qortal/transaction/UpdateAssetTransaction.java b/src/main/java/org/qortal/transaction/UpdateAssetTransaction.java index 2a7af23c..16e5641d 100644 --- a/src/main/java/org/qortal/transaction/UpdateAssetTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateAssetTransaction.java @@ -90,6 +90,11 @@ public class UpdateAssetTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Asset diff --git a/src/main/java/org/qortal/transaction/UpdateGroupTransaction.java b/src/main/java/org/qortal/transaction/UpdateGroupTransaction.java index 6751be33..9664ccbf 100644 --- a/src/main/java/org/qortal/transaction/UpdateGroupTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateGroupTransaction.java @@ -109,6 +109,11 @@ public class UpdateGroupTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { // Update Group diff --git a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java index ebfde97c..c9eedbae 100644 --- a/src/main/java/org/qortal/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qortal/transaction/UpdateNameTransaction.java @@ -2,9 +2,11 @@ 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.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.TransactionData; @@ -124,6 +126,22 @@ public class UpdateNameTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; + + // Rebuild this name in the Names table from the transaction history + // This is necessary because in some rare cases names can be missing from the Names table after registration + // but we have been unable to reproduce the issue and track down the root cause + NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck(); + namesDatabaseIntegrityCheck.rebuildName(updateNameTransactionData.getName(), this.repository); + + if (!Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) { + // Renaming - so make sure the new name is rebuilt too + namesDatabaseIntegrityCheck.rebuildName(updateNameTransactionData.getNewName(), this.repository); + } + } + @Override public void process() throws DataException { // Update Name diff --git a/src/main/java/org/qortal/transaction/VoteOnPollTransaction.java b/src/main/java/org/qortal/transaction/VoteOnPollTransaction.java index 35447aa6..89eec184 100644 --- a/src/main/java/org/qortal/transaction/VoteOnPollTransaction.java +++ b/src/main/java/org/qortal/transaction/VoteOnPollTransaction.java @@ -92,6 +92,11 @@ public class VoteOnPollTransaction extends Transaction { return ValidationResult.OK; } + @Override + public void preProcess() throws DataException { + // Nothing to do + } + @Override public void process() throws DataException { String pollName = this.voteOnPollTransactionData.getPollName(); diff --git a/src/test/java/org/qortal/test/naming/IntegrityTests.java b/src/test/java/org/qortal/test/naming/IntegrityTests.java new file mode 100644 index 00000000..d278cf3a --- /dev/null +++ b/src/test/java/org/qortal/test/naming/IntegrityTests.java @@ -0,0 +1,345 @@ +package org.qortal.test.naming; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; +import org.qortal.data.transaction.*; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction; + +import static org.junit.Assert.*; + +public class IntegrityTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testValidName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Run the database integrity check for this name + NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck(); + assertEquals(1, integrityCheck.rebuildName(name, repository)); + + // Ensure the name still exists and the data is still correct + assertEquals(data, repository.getNameRepository().fromName(name).getData()); + } + } + + @Test + public void testMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Run the database integrity check for this name and check that a row was modified + NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck(); + assertEquals(1, integrityCheck.rebuildName(name, repository)); + + // Ensure the name exists again and the data is correct + assertEquals(data, repository.getNameRepository().fromName(name).getData()); + } + } + + @Test + public void testMissingNameAfterUpdate() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Update the name + String newData = "{\"age\":31}"; + UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, name, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Ensure the name still exists and the data has been updated + assertEquals(newData, repository.getNameRepository().fromName(name).getData()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Run the database integrity check for this name + // We expect 2 modifications to be made - the original register name followed by the update + NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck(); + assertEquals(2, integrityCheck.rebuildName(name, repository)); + + // Ensure the name exists and the data is correct + assertEquals(newData, repository.getNameRepository().fromName(name).getData()); + } + } + + @Test + public void testMissingNameAfterRename() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Rename the name + String newName = "new-name"; + String newData = "{\"age\":31}"; + UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData); + TransactionUtils.signAndMint(repository, updateTransactionData, alice); + + // Ensure the new name exists and the data has been updated + assertEquals(newData, repository.getNameRepository().fromName(newName).getData()); + + // Ensure the old name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Now delete the new name, to simulate a database inconsistency + repository.getNameRepository().delete(newName); + + // Ensure the new name doesn't exist + assertNull(repository.getNameRepository().fromName(newName)); + + // Attempt to register the new name + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, data); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + // Transaction should be invalid, because the database inconsistency was fixed by RegisterNameTransaction.preProcess() + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result); + assertTrue("Name should already be registered", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result); + } + } + + @Test + public void testRegisterMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Attempt to register the name again + String duplicateName = "TEST-nÁme"; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data); + Transaction transaction = Transaction.fromData(repository, transactionData); + transaction.sign(alice); + + // Transaction should be invalid, because the database inconsistency was fixed by RegisterNameTransaction.preProcess() + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result); + assertTrue("Name should already be registered", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result); + } + } + + @Test + public void testUpdateMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "test-name"; + String data = "{\"age\":30}"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Ensure the name exists and the data is correct + assertEquals(data, repository.getNameRepository().fromName(initialName).getData()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(initialName); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(initialName)); + + // Attempt to update the name + String newName = "new-name"; + String newData = ""; + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData); + Transaction transaction = Transaction.fromData(repository, updateTransactionData); + transaction.sign(alice); + + // Transaction should be valid, because the database inconsistency was fixed by UpdateNameTransaction.preProcess() + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result); + } + } + + @Test + public void testUpdateToMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String initialName = "test-name"; + String data = "{\"age\":30}"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Ensure the name exists and the data is correct + assertEquals(data, repository.getNameRepository().fromName(initialName).getData()); + + // Register the second name that we will ultimately try and rename the first name to + String secondName = "new-missing-name"; + String secondNameData = "{\"data2\":true}"; + transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, secondNameData); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Ensure the second name exists and the data is correct + assertEquals(secondNameData, repository.getNameRepository().fromName(secondName).getData()); + + // Now delete the second name, to simulate a database inconsistency + repository.getNameRepository().delete(secondName); + + // Ensure the second name doesn't exist + assertNull(repository.getNameRepository().fromName(secondName)); + + // Attempt to rename the first name to the second name + TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, secondName, secondNameData); + Transaction transaction = Transaction.fromData(repository, updateTransactionData); + transaction.sign(alice); + + // Transaction should be invalid, because the database inconsistency was fixed by UpdateNameTransaction.preProcess() + // Therefore the name that we are trying to rename TO already exists + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result); + assertTrue("Destination name should already exist", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result); + } + } + + @Test + public void testSellMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Attempt to sell the name + TransactionData sellTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, 123456); + Transaction transaction = Transaction.fromData(repository, sellTransactionData); + transaction.sign(alice); + + // Transaction should be valid, because the database inconsistency was fixed by SellNameTransaction.preProcess() + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result); + } + } + + @Test + public void testBuyMissingName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + String data = "{\"age\":30}"; + + 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()); + + // Now delete the name, to simulate a database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Attempt to sell the name + long amount = 123456; + TransactionData sellTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, amount); + TransactionUtils.signAndMint(repository, sellTransactionData, alice); + + // Ensure the name now exists + assertNotNull(repository.getNameRepository().fromName(name)); + + // Now delete the name again, to simulate another database inconsistency + repository.getNameRepository().delete(name); + + // Ensure the name doesn't exist + assertNull(repository.getNameRepository().fromName(name)); + + // Bob now attempts to buy the name + String seller = alice.getAddress(); + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + TransactionData buyTransactionData = new BuyNameTransactionData(TestTransaction.generateBase(bob), name, amount, seller); + Transaction transaction = Transaction.fromData(repository, buyTransactionData); + transaction.sign(bob); + + // Transaction should be valid, because the database inconsistency was fixed by SellNameTransaction.preProcess() + Transaction.ValidationResult result = transaction.importAsUnconfirmed(); + assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result); + } + } + +}