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.
This commit is contained in:
CalDescent 2021-09-22 08:15:23 +01:00
parent 39d5ce19e2
commit 449761b6ca
47 changed files with 877 additions and 151 deletions

View File

@ -41,12 +41,14 @@ import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData; import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.BlockTransactionData; import org.qortal.data.block.BlockTransactionData;
import org.qortal.data.network.OnlineAccountData; import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.ATRepository; import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.TransactionRepository; import org.qortal.repository.TransactionRepository;
import org.qortal.transaction.AtTransaction; import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.ApprovalStatus;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
@ -1282,6 +1284,21 @@ public class Block {
return mintingAccount.canMint(); 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<Transaction> blocksTransactions = this.getTransactions();
for (Transaction transaction : blocksTransactions) {
transaction.preProcess();
}
}
/** /**
* Process block, and its transactions, adding them to the blockchain. * Process block, and its transactions, adding them to the blockchain.
* *

View File

@ -249,6 +249,8 @@ public class BlockMinter extends Thread {
if (testBlock.isTimestampValid() != ValidationResult.OK) if (testBlock.isTimestampValid() != ValidationResult.OK)
continue; continue;
testBlock.preProcess();
// Is new block valid yet? (Before adding unconfirmed transactions) // Is new block valid yet? (Before adding unconfirmed transactions)
ValidationResult result = testBlock.isValid(); ValidationResult result = testBlock.isValid();
if (result != ValidationResult.OK) { if (result != ValidationResult.OK) {

View File

@ -429,8 +429,9 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error 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 namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildAllNames();
namesDatabaseIntegrityCheck.runIntegrityCheck(); namesDatabaseIntegrityCheck.runIntegrityCheck();
LOGGER.info("Validating blockchain"); LOGGER.info("Validating blockchain");

View File

@ -1064,6 +1064,8 @@ public class Synchronizer {
if (Controller.isStopping()) if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN; return SynchronizationResult.SHUTTING_DOWN;
newBlock.preProcess();
ValidationResult blockResult = newBlock.isValid(); ValidationResult blockResult = newBlock.isValid();
if (blockResult != ValidationResult.OK) { if (blockResult != ValidationResult.OK) {
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, 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()) for (Transaction transaction : newBlock.getTransactions())
transaction.setInitialApprovalStatus(); transaction.setInitialApprovalStatus();
newBlock.preProcess();
ValidationResult blockResult = newBlock.isValid(); ValidationResult blockResult = newBlock.isValid();
if (blockResult != ValidationResult.OK) { if (blockResult != ValidationResult.OK) {
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,

View File

@ -5,16 +5,13 @@ import org.apache.logging.log4j.Logger;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.naming.NameData; import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData; import org.qortal.data.transaction.*;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateNameTransactionData;
import org.qortal.naming.Name; import org.qortal.naming.Name;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager; import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58; import org.qortal.utils.Unicode;
import java.util.*; import java.util.*;
@ -22,31 +19,127 @@ public class NamesDatabaseIntegrityCheck {
private static final Logger LOGGER = LogManager.getLogger(NamesDatabaseIntegrityCheck.class); private static final Logger LOGGER = LogManager.getLogger(NamesDatabaseIntegrityCheck.class);
private static final List<TransactionType> REGISTER_NAME_TX_TYPE = Collections.singletonList(TransactionType.REGISTER_NAME); private static final List<TransactionType> ALL_NAME_TX_TYPE = Arrays.asList(
private static final List<TransactionType> UPDATE_NAME_TX_TYPE = Collections.singletonList(TransactionType.UPDATE_NAME); TransactionType.REGISTER_NAME,
private static final List<TransactionType> BUY_NAME_TX_TYPE = Collections.singletonList(TransactionType.BUY_NAME); TransactionType.UPDATE_NAME,
TransactionType.BUY_NAME,
TransactionType.SELL_NAME
);
private List<RegisterNameTransactionData> registerNameTransactions; private List<TransactionData> nameTransactions = new ArrayList<>();
private List<UpdateNameTransactionData> updateNameTransactions;
private List<BuyNameTransactionData> buyNameTransactions; public int rebuildName(String name, Repository repository) {
int modificationCount = 0;
try {
List<TransactionData> 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<String> 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() { public void runIntegrityCheck() {
boolean integrityCheckFailed = false; boolean integrityCheckFailed = false;
boolean corrected = false;
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
// Fetch all the (confirmed) name-related transactions // Fetch all the (confirmed) REGISTER_NAME transactions
this.fetchRegisterNameTransactions(repository); List<RegisterNameTransactionData> registerNameTransactions = this.fetchRegisterNameTransactions();
this.fetchUpdateNameTransactions(repository);
this.fetchBuyNameTransactions(repository);
// Loop through each REGISTER_NAME txn signature and request the full transaction data // 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(); String registeredName = registerNameTransactionData.getName();
NameData nameData = repository.getNameRepository().fromName(registeredName); NameData nameData = repository.getNameRepository().fromName(registeredName);
// Check to see if this name has been updated or bought at any point // 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) { if (latestUpdate == null) {
// Name was never updated once registered // Name was never updated once registered
// We expect this name to still be registered to this transaction's creator // We expect this name to still be registered to this transaction's creator
@ -54,16 +147,9 @@ public class NamesDatabaseIntegrityCheck {
if (nameData == null) { if (nameData == null) {
LOGGER.info("Error: registered name {} doesn't exist in Names table. Adding...", registeredName); LOGGER.info("Error: registered name {} doesn't exist in Names table. Adding...", registeredName);
integrityCheckFailed = true; integrityCheckFailed = true;
// Register the name
Name name = new Name(repository, registerNameTransactionData);
name.register();
repository.saveChanges();
corrected = true;
continue;
} }
else { else {
//LOGGER.info("Registered name {} is correctly registered", registeredName); LOGGER.trace("Registered name {} is correctly registered", registeredName);
} }
// Check the owner is correct // Check the owner is correct
@ -72,18 +158,16 @@ public class NamesDatabaseIntegrityCheck {
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
registeredName, nameData.getOwner(), creator.getAddress()); registeredName, nameData.getOwner(), creator.getAddress());
integrityCheckFailed = true; integrityCheckFailed = true;
// FUTURE: Fix the name's owner if we ever see the above log entry
} }
else { else {
//LOGGER.info("Registered name {} has the correct owner", registeredName); LOGGER.trace("Registered name {} has the correct owner", registeredName);
} }
} }
else { else {
// Check if owner is correct after update // Check if owner is correct after update
// Check for name updates // Check for name updates
if (latestUpdate instanceof UpdateNameTransactionData) { if (latestUpdate.getType() == TransactionType.UPDATE_NAME) {
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) latestUpdate; UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) latestUpdate;
PublicKeyAccount creator = new PublicKeyAccount(repository, updateNameTransactionData.getCreatorPublicKey()); 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 {}", LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
registeredName, nameData.getOwner(), creator.getAddress()); registeredName, nameData.getOwner(), creator.getAddress());
integrityCheckFailed = true; integrityCheckFailed = true;
}
// FUTURE: Fix the name's owner if we ever see the above log entry else {
} else { LOGGER.trace("Registered name {} has the correct owner after being updated", registeredName);
//LOGGER.info("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 {}", LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
updateNameTransactionData.getNewName(), newNameData.getOwner(), creator.getAddress()); updateNameTransactionData.getNewName(), newNameData.getOwner(), creator.getAddress());
integrityCheckFailed = true; integrityCheckFailed = true;
}
// FUTURE: Fix the name's owner if we ever see the above log entry else {
} else { LOGGER.trace("Registered name {} has the correct owner after being updated", updateNameTransactionData.getNewName());
//LOGGER.info("Registered name {} has the correct owner after being updated", updateNameTransactionData.getNewName());
} }
} }
@ -121,18 +203,31 @@ public class NamesDatabaseIntegrityCheck {
} }
} }
// Check for name sales // Check for name buys
else if (latestUpdate instanceof BuyNameTransactionData) { else if (latestUpdate.getType() == TransactionType.BUY_NAME) {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) latestUpdate; BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) latestUpdate;
PublicKeyAccount creator = new PublicKeyAccount(repository, buyNameTransactionData.getCreatorPublicKey()); PublicKeyAccount creator = new PublicKeyAccount(repository, buyNameTransactionData.getCreatorPublicKey());
if (!Objects.equals(creator.getAddress(), nameData.getOwner())) { if (!Objects.equals(creator.getAddress(), nameData.getOwner())) {
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}", LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
registeredName, nameData.getOwner(), creator.getAddress()); registeredName, nameData.getOwner(), creator.getAddress());
integrityCheckFailed = true; 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 // Check for name sells
} else { else if (latestUpdate.getType() == TransactionType.SELL_NAME) {
//LOGGER.info("Registered name {} has the correct owner after being bought", registeredName); 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 (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 { } else {
LOGGER.info("Registered names database integrity check passed."); LOGGER.info("Registered names database integrity check passed.");
} }
} }
private void fetchRegisterNameTransactions(Repository repository) throws DataException { private List<RegisterNameTransactionData> fetchRegisterNameTransactions() {
List<RegisterNameTransactionData> registerNameTransactions = new ArrayList<>(); List<RegisterNameTransactionData> registerNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures for (TransactionData transactionData : this.nameTransactions) {
List<byte[]> registerNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( if (transactionData.getType() == TransactionType.REGISTER_NAME) {
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;
}
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData; RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
registerNameTransactions.add(registerNameTransactionData); registerNameTransactions.add(registerNameTransactionData);
} }
this.registerNameTransactions = registerNameTransactions; }
return registerNameTransactions;
} }
private void fetchUpdateNameTransactions(Repository repository) throws DataException { private List<UpdateNameTransactionData> fetchUpdateNameTransactions() {
List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>(); List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures for (TransactionData transactionData : this.nameTransactions) {
List<byte[]> updateNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( if (transactionData.getType() == TransactionType.UPDATE_NAME) {
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;
}
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
updateNameTransactions.add(updateNameTransactionData); updateNameTransactions.add(updateNameTransactionData);
} }
this.updateNameTransactions = updateNameTransactions; }
return updateNameTransactions;
} }
private void fetchBuyNameTransactions(Repository repository) throws DataException { private List<SellNameTransactionData> fetchSellNameTransactions() {
List<SellNameTransactionData> 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<BuyNameTransactionData> fetchBuyNameTransactions() {
List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>(); List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures for (TransactionData transactionData : this.nameTransactions) {
List<byte[]> buyNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria( if (transactionData.getType() == TransactionType.BUY_NAME) {
null, null, null, BUY_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));
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; BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
buyNameTransactions.add(buyNameTransactionData); buyNameTransactions.add(buyNameTransactionData);
} }
this.buyNameTransactions = buyNameTransactions; }
return buyNameTransactions;
} }
private List<UpdateNameTransactionData> fetchUpdateTransactionsInvolvingName(String registeredName) { private void fetchAllNameTransactions(Repository repository) throws DataException {
List<UpdateNameTransactionData> matchedTransactions = new ArrayList<>(); List<TransactionData> nameTransactions = new ArrayList<>();
for (UpdateNameTransactionData updateNameTransactionData : this.updateNameTransactions) { // Fetch all the confirmed REGISTER_NAME transaction signatures
if (Objects.equals(updateNameTransactionData.getName(), registeredName) || List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(
Objects.equals(updateNameTransactionData.getNewName(), registeredName)) { null, null, null, ALL_NAME_TX_TYPE, null, null,
ConfirmationStatus.CONFIRMED, null, null, false);
matchedTransactions.add(updateNameTransactionData); for (byte[] signature : signatures) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
nameTransactions.add(transactionData);
} }
} this.nameTransactions = nameTransactions;
return matchedTransactions;
} }
private List<BuyNameTransactionData> fetchBuyTransactionsInvolvingName(String registeredName) { private List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
List<BuyNameTransactionData> matchedTransactions = new ArrayList<>(); List<TransactionData> transactions = new ArrayList<>();
String reducedName = Unicode.sanitize(name);
for (BuyNameTransactionData buyNameTransactionData : this.buyNameTransactions) { // Fetch all the confirmed name-modification transactions
if (Objects.equals(buyNameTransactionData.getName(), registeredName)) { if (this.nameTransactions.isEmpty()) {
this.fetchAllNameTransactions(repository);
matchedTransactions.add(buyNameTransactionData);
}
}
return matchedTransactions;
} }
private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName) { for (TransactionData transactionData : this.nameTransactions) {
List<TransactionData> latestTransactions = new ArrayList<>();
List<UpdateNameTransactionData> updates = this.fetchUpdateTransactionsInvolvingName(registeredName); if ((transactionData instanceof RegisterNameTransactionData)) {
List<BuyNameTransactionData> buys = this.fetchBuyTransactionsInvolvingName(registeredName); RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
if (Objects.equals(registerNameTransactionData.getReducedName(), reducedName)) {
// Get the latest updates for this name transactions.add(transactionData);
UpdateNameTransactionData latestUpdateToName = updates.stream() }
.filter(update -> update.getNewName().equals(registeredName)) }
.max(Comparator.comparing(UpdateNameTransactionData::getTimestamp)) if ((transactionData instanceof UpdateNameTransactionData)) {
.orElse(null); UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
if (latestUpdateToName != null) { if (Objects.equals(updateNameTransactionData.getName(), name) ||
latestTransactions.add(latestUpdateToName); 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 transactions;
} }
UpdateNameTransactionData latestUpdateFromName = updates.stream() private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName, Repository repository) throws DataException {
.filter(update -> update.getName().equals(registeredName)) List<TransactionData> transactionsInvolvingName = this.fetchAllTransactionsInvolvingName(registeredName, repository);
.max(Comparator.comparing(UpdateNameTransactionData::getTimestamp))
.orElse(null);
if (latestUpdateFromName != null) {
latestTransactions.add(latestUpdateFromName);
}
// Get the latest buy for this name // Get the latest update for this name (excluding REGISTER_NAME transactions)
BuyNameTransactionData latestBuyForName = buys.stream() TransactionData latestUpdateToName = transactionsInvolvingName.stream()
.filter(update -> update.getName().equals(registeredName)) .filter(txn -> txn.getType() != TransactionType.REGISTER_NAME)
.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()
.max(Comparator.comparing(TransactionData::getTimestamp)) .max(Comparator.comparing(TransactionData::getTimestamp))
.orElse(null); .orElse(null);
return latestUpdate; return latestUpdateToName;
}
private List<String> fetchAllNames(Repository repository) throws DataException {
List<String> 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;
} }
} }

View File

@ -265,4 +265,8 @@ public class Name {
return previousTransactionData.getTimestamp(); return previousTransactionData.getTimestamp();
} }
public NameData getNameData() {
return this.nameData;
}
} }

View File

@ -48,6 +48,11 @@ public class AccountFlagsTransaction extends Transaction {
return ValidationResult.NO_FLAG_PERMISSION; return ValidationResult.NO_FLAG_PERMISSION;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
Account target = this.getTarget(); Account target = this.getTarget();

View File

@ -49,6 +49,11 @@ public class AccountLevelTransaction extends Transaction {
return ValidationResult.NO_FLAG_PERMISSION; return ValidationResult.NO_FLAG_PERMISSION;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
Account target = getTarget(); Account target = getTarget();

View File

@ -84,6 +84,11 @@ public class AddGroupAdminTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group adminship // Update Group adminship

View File

@ -60,6 +60,11 @@ public class ArbitraryTransaction extends Transaction {
arbitraryTransactionData.getFee()); arbitraryTransactionData.getFee());
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Wrap and delegate payment processing to Payment class. // Wrap and delegate payment processing to Payment class.

View File

@ -80,6 +80,11 @@ public class AtTransaction extends Transaction {
return Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference()); return Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference());
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Check recipient address is valid // Check recipient address is valid

View File

@ -6,6 +6,7 @@ import java.util.List;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData; import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData; import org.qortal.data.transaction.BuyNameTransactionData;
@ -98,6 +99,17 @@ public class BuyNameTransaction extends Transaction {
return ValidationResult.OK; 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 @Override
public void process() throws DataException { public void process() throws DataException {
// Buy Name // Buy Name

View File

@ -62,6 +62,11 @@ public class CancelAssetOrderTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Mark Order as completed so no more trades can happen // Mark Order as completed so no more trades can happen

View File

@ -83,6 +83,11 @@ public class CancelGroupBanTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -83,6 +83,11 @@ public class CancelGroupInviteTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -79,6 +79,11 @@ public class CancelSellNameTransaction extends Transaction {
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Name // Update Name

View File

@ -135,6 +135,11 @@ public class ChatTransaction extends Transaction {
return true; return true;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import // Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@ -135,6 +135,11 @@ public class CreateAssetOrderTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Order Id is transaction's signature // Order Id is transaction's signature

View File

@ -92,6 +92,11 @@ public class CreateGroupTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Create Group // Create Group

View File

@ -106,6 +106,11 @@ public class CreatePollTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Publish poll to allow voting // Publish poll to allow voting

View File

@ -203,6 +203,11 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
ensureATAddress(this.deployAtTransactionData); ensureATAddress(this.deployAtTransactionData);

View File

@ -100,6 +100,11 @@ public class GenesisTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
Account recipient = new Account(repository, this.genesisTransactionData.getRecipient()); Account recipient = new Account(repository, this.genesisTransactionData.getRecipient());

View File

@ -66,6 +66,11 @@ public class GroupApprovalTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Find previous approval decision (if any) by this admin for pending transaction // Find previous approval decision (if any) by this admin for pending transaction

View File

@ -87,6 +87,11 @@ public class GroupBanTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -88,6 +88,11 @@ public class GroupInviteTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -89,6 +89,11 @@ public class GroupKickTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -92,6 +92,11 @@ public class IssueAssetTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Issue asset // Issue asset

View File

@ -67,6 +67,11 @@ public class JoinGroupTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -67,6 +67,11 @@ public class LeaveGroupTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group Membership // Update Group Membership

View File

@ -239,6 +239,11 @@ public class MessageTransaction extends Transaction {
getPaymentData(), this.messageTransactionData.getFee(), true); getPaymentData(), this.messageTransactionData.getFee(), true);
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// If we have no amount then there's nothing to do // If we have no amount then there's nothing to do

View File

@ -67,6 +67,11 @@ public class MultiPaymentTransaction extends Transaction {
return new Payment(this.repository).isProcessable(this.multiPaymentTransactionData.getSenderPublicKey(), payments, this.multiPaymentTransactionData.getFee()); return new Payment(this.repository).isProcessable(this.multiPaymentTransactionData.getSenderPublicKey(), payments, this.multiPaymentTransactionData.getFee());
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Wrap and delegate payment processing to Payment class. // Wrap and delegate payment processing to Payment class.

View File

@ -61,6 +61,11 @@ public class PaymentTransaction extends Transaction {
return new Payment(this.repository).isProcessable(this.paymentTransactionData.getSenderPublicKey(), getPaymentData(), this.paymentTransactionData.getFee()); return new Payment(this.repository).isProcessable(this.paymentTransactionData.getSenderPublicKey(), getPaymentData(), this.paymentTransactionData.getFee());
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Wrap and delegate payment processing to Payment class. // Wrap and delegate payment processing to Payment class.

View File

@ -149,6 +149,11 @@ public class PresenceTransaction extends Transaction {
return true; return true;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import // Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@ -80,6 +80,11 @@ public class PublicizeTransaction extends Transaction {
return true; return true;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// There can be only one // There can be only one

View File

@ -6,6 +6,7 @@ import java.util.List;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.RegisterNameTransactionData; import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -88,6 +89,17 @@ public class RegisterNameTransaction extends Transaction {
return ValidationResult.OK; 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 @Override
public void process() throws DataException { public void process() throws DataException {
// Register Name // Register Name

View File

@ -87,6 +87,11 @@ public class RemoveGroupAdminTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group adminship // Update Group adminship

View File

@ -159,6 +159,11 @@ public class RewardShareTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
PublicKeyAccount mintingAccount = getMintingAccount(); PublicKeyAccount mintingAccount = getMintingAccount();

View File

@ -5,6 +5,7 @@ import java.util.List;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.data.naming.NameData; import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.SellNameTransactionData; import org.qortal.data.transaction.SellNameTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -89,6 +90,17 @@ public class SellNameTransaction extends Transaction {
return ValidationResult.OK; 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 @Override
public void process() throws DataException { public void process() throws DataException {
// Sell Name // Sell Name

View File

@ -56,6 +56,11 @@ public class SetGroupTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
Account creator = getCreator(); Account creator = getCreator();

View File

@ -791,6 +791,8 @@ public abstract class Transaction {
// Fix up approval status // Fix up approval status
this.setInitialApprovalStatus(); this.setInitialApprovalStatus();
this.preProcess();
ValidationResult validationResult = this.isValidUnconfirmed(); ValidationResult validationResult = this.isValidUnconfirmed();
if (validationResult != ValidationResult.OK) if (validationResult != ValidationResult.OK)
return validationResult; return validationResult;
@ -891,6 +893,14 @@ public abstract class Transaction {
return ValidationResult.OK; 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. * Actually process a transaction, updating the blockchain.
* <p> * <p>

View File

@ -61,6 +61,11 @@ public class TransferAssetTransaction extends Transaction {
return new Payment(this.repository).isProcessable(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee()); return new Payment(this.repository).isProcessable(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee());
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Wrap asset transfer as a payment and delegate processing to Payment class. // Wrap asset transfer as a payment and delegate processing to Payment class.

View File

@ -68,6 +68,11 @@ public class TransferPrivsTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
Account sender = this.getSender(); Account sender = this.getSender();

View File

@ -90,6 +90,11 @@ public class UpdateAssetTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Asset // Update Asset

View File

@ -109,6 +109,11 @@ public class UpdateGroupTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
// Update Group // Update Group

View File

@ -2,9 +2,11 @@ package org.qortal.transaction;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData; import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -124,6 +126,22 @@ public class UpdateNameTransaction extends Transaction {
return ValidationResult.OK; 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 @Override
public void process() throws DataException { public void process() throws DataException {
// Update Name // Update Name

View File

@ -92,6 +92,11 @@ public class VoteOnPollTransaction extends Transaction {
return ValidationResult.OK; return ValidationResult.OK;
} }
@Override
public void preProcess() throws DataException {
// Nothing to do
}
@Override @Override
public void process() throws DataException { public void process() throws DataException {
String pollName = this.voteOnPollTransactionData.getPollName(); String pollName = this.voteOnPollTransactionData.getPollName();

View File

@ -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);
}
}
}