From 449761b6ca39a00da5fe427ddba3d2fb8dcfd3a8 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 22 Sep 2021 08:15:23 +0100 Subject: [PATCH] Rework of "Names" integrity check Problem: The "Names" table (the latest state of each name) drifts out of sync with the name-related transaction history on a subset of nodes for some unknown and seemingly difficult to find reason. Solution: Treat the "Names" table as a cache that can be rebuilt at any time. It now works like this: - On node startup, rebuild the entire Names table by replaying the transaction history of all registered names. Includes registrations, updates, buys and sells. - Add a "pre-process" stage to block/transaction processing. If the block contains a name related transaction, rebuild the Names cache for any names referenced by these transactions before validating anything. The existing "integrity check" has been modified to just check basic attributes based on the latest transaction for a name. It will log if there are any inconsistencies found, but won't correct anything. This adds confidence that the rebuild has worked correctly. There are also multiple unit tests to ensure that the rebuilds are coping with various different scenarios. --- src/main/java/org/qortal/block/Block.java | 17 + .../org/qortal/controller/BlockMinter.java | 2 + .../org/qortal/controller/Controller.java | 3 +- .../org/qortal/controller/Synchronizer.java | 4 + .../NamesDatabaseIntegrityCheck.java | 410 +++++++++++------- src/main/java/org/qortal/naming/Name.java | 4 + .../transaction/AccountFlagsTransaction.java | 5 + .../transaction/AccountLevelTransaction.java | 5 + .../transaction/AddGroupAdminTransaction.java | 7 +- .../transaction/ArbitraryTransaction.java | 5 + .../org/qortal/transaction/AtTransaction.java | 5 + .../transaction/BuyNameTransaction.java | 12 + .../CancelAssetOrderTransaction.java | 5 + .../CancelGroupBanTransaction.java | 5 + .../CancelGroupInviteTransaction.java | 5 + .../CancelSellNameTransaction.java | 5 + .../qortal/transaction/ChatTransaction.java | 5 + .../CreateAssetOrderTransaction.java | 5 + .../transaction/CreateGroupTransaction.java | 5 + .../transaction/CreatePollTransaction.java | 5 + .../transaction/DeployAtTransaction.java | 5 + .../transaction/GenesisTransaction.java | 5 + .../transaction/GroupApprovalTransaction.java | 5 + .../transaction/GroupBanTransaction.java | 5 + .../transaction/GroupInviteTransaction.java | 5 + .../transaction/GroupKickTransaction.java | 5 + .../transaction/IssueAssetTransaction.java | 5 + .../transaction/JoinGroupTransaction.java | 5 + .../transaction/LeaveGroupTransaction.java | 5 + .../transaction/MessageTransaction.java | 5 + .../transaction/MultiPaymentTransaction.java | 5 + .../transaction/PaymentTransaction.java | 5 + .../transaction/PresenceTransaction.java | 5 + .../transaction/PublicizeTransaction.java | 5 + .../transaction/RegisterNameTransaction.java | 12 + .../RemoveGroupAdminTransaction.java | 7 +- .../transaction/RewardShareTransaction.java | 5 + .../transaction/SellNameTransaction.java | 12 + .../transaction/SetGroupTransaction.java | 5 + .../org/qortal/transaction/Transaction.java | 10 + .../transaction/TransferAssetTransaction.java | 5 + .../transaction/TransferPrivsTransaction.java | 5 + .../transaction/UpdateAssetTransaction.java | 5 + .../transaction/UpdateGroupTransaction.java | 5 + .../transaction/UpdateNameTransaction.java | 18 + .../transaction/VoteOnPollTransaction.java | 5 + .../qortal/test/naming/IntegrityTests.java | 345 +++++++++++++++ 47 files changed, 877 insertions(+), 151 deletions(-) create mode 100644 src/test/java/org/qortal/test/naming/IntegrityTests.java 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); + } + } + +}