Browse Source

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.
block-archive
CalDescent 3 years ago
parent
commit
449761b6ca
  1. 17
      src/main/java/org/qortal/block/Block.java
  2. 2
      src/main/java/org/qortal/controller/BlockMinter.java
  3. 3
      src/main/java/org/qortal/controller/Controller.java
  4. 4
      src/main/java/org/qortal/controller/Synchronizer.java
  5. 396
      src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java
  6. 4
      src/main/java/org/qortal/naming/Name.java
  7. 5
      src/main/java/org/qortal/transaction/AccountFlagsTransaction.java
  8. 5
      src/main/java/org/qortal/transaction/AccountLevelTransaction.java
  9. 7
      src/main/java/org/qortal/transaction/AddGroupAdminTransaction.java
  10. 5
      src/main/java/org/qortal/transaction/ArbitraryTransaction.java
  11. 5
      src/main/java/org/qortal/transaction/AtTransaction.java
  12. 12
      src/main/java/org/qortal/transaction/BuyNameTransaction.java
  13. 5
      src/main/java/org/qortal/transaction/CancelAssetOrderTransaction.java
  14. 5
      src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java
  15. 5
      src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java
  16. 5
      src/main/java/org/qortal/transaction/CancelSellNameTransaction.java
  17. 5
      src/main/java/org/qortal/transaction/ChatTransaction.java
  18. 5
      src/main/java/org/qortal/transaction/CreateAssetOrderTransaction.java
  19. 5
      src/main/java/org/qortal/transaction/CreateGroupTransaction.java
  20. 5
      src/main/java/org/qortal/transaction/CreatePollTransaction.java
  21. 5
      src/main/java/org/qortal/transaction/DeployAtTransaction.java
  22. 5
      src/main/java/org/qortal/transaction/GenesisTransaction.java
  23. 5
      src/main/java/org/qortal/transaction/GroupApprovalTransaction.java
  24. 5
      src/main/java/org/qortal/transaction/GroupBanTransaction.java
  25. 5
      src/main/java/org/qortal/transaction/GroupInviteTransaction.java
  26. 5
      src/main/java/org/qortal/transaction/GroupKickTransaction.java
  27. 5
      src/main/java/org/qortal/transaction/IssueAssetTransaction.java
  28. 5
      src/main/java/org/qortal/transaction/JoinGroupTransaction.java
  29. 5
      src/main/java/org/qortal/transaction/LeaveGroupTransaction.java
  30. 5
      src/main/java/org/qortal/transaction/MessageTransaction.java
  31. 5
      src/main/java/org/qortal/transaction/MultiPaymentTransaction.java
  32. 5
      src/main/java/org/qortal/transaction/PaymentTransaction.java
  33. 5
      src/main/java/org/qortal/transaction/PresenceTransaction.java
  34. 5
      src/main/java/org/qortal/transaction/PublicizeTransaction.java
  35. 12
      src/main/java/org/qortal/transaction/RegisterNameTransaction.java
  36. 7
      src/main/java/org/qortal/transaction/RemoveGroupAdminTransaction.java
  37. 5
      src/main/java/org/qortal/transaction/RewardShareTransaction.java
  38. 12
      src/main/java/org/qortal/transaction/SellNameTransaction.java
  39. 5
      src/main/java/org/qortal/transaction/SetGroupTransaction.java
  40. 10
      src/main/java/org/qortal/transaction/Transaction.java
  41. 5
      src/main/java/org/qortal/transaction/TransferAssetTransaction.java
  42. 5
      src/main/java/org/qortal/transaction/TransferPrivsTransaction.java
  43. 5
      src/main/java/org/qortal/transaction/UpdateAssetTransaction.java
  44. 5
      src/main/java/org/qortal/transaction/UpdateGroupTransaction.java
  45. 18
      src/main/java/org/qortal/transaction/UpdateNameTransaction.java
  46. 5
      src/main/java/org/qortal/transaction/VoteOnPollTransaction.java
  47. 345
      src/test/java/org/qortal/test/naming/IntegrityTests.java

17
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<Transaction> blocksTransactions = this.getTransactions();
for (Transaction transaction : blocksTransactions) {
transaction.preProcess();
}
}
/**
* Process block, and its transactions, adding them to the blockchain.
*

2
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) {

3
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");

4
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,

396
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<TransactionType> REGISTER_NAME_TX_TYPE = Collections.singletonList(TransactionType.REGISTER_NAME);
private static final List<TransactionType> UPDATE_NAME_TX_TYPE = Collections.singletonList(TransactionType.UPDATE_NAME);
private static final List<TransactionType> BUY_NAME_TX_TYPE = Collections.singletonList(TransactionType.BUY_NAME);
private static final List<TransactionType> ALL_NAME_TX_TYPE = Arrays.asList(
TransactionType.REGISTER_NAME,
TransactionType.UPDATE_NAME,
TransactionType.BUY_NAME,
TransactionType.SELL_NAME
);
private List<TransactionData> nameTransactions = new ArrayList<>();
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()));
}
}
private List<RegisterNameTransactionData> registerNameTransactions;
private List<UpdateNameTransactionData> updateNameTransactions;
private List<BuyNameTransactionData> buyNameTransactions;
// 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() {
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<RegisterNameTransactionData> 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<RegisterNameTransactionData> fetchRegisterNameTransactions() {
List<RegisterNameTransactionData> registerNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures
List<byte[]> 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<UpdateNameTransactionData> fetchUpdateNameTransactions() {
List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures
List<byte[]> updateNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria(
null, null, null, UPDATE_NAME_TX_TYPE, null, null,
ConfirmationStatus.CONFIRMED, null, null, false);
for (TransactionData transactionData : this.nameTransactions) {
if (transactionData.getType() == TransactionType.UPDATE_NAME) {
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
updateNameTransactions.add(updateNameTransactionData);
}
}
return updateNameTransactions;
}
for (byte[] signature : updateNameSigs) {
// LOGGER.info("Fetching UPDATE_NAME transaction from signature {}...", Base58.encode(signature));
private List<SellNameTransactionData> fetchSellNameTransactions() {
List<SellNameTransactionData> sellNameTransactions = new ArrayList<>();
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.SELL_NAME) {
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
sellNameTransactions.add(sellNameTransactionData);
}
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
updateNameTransactions.add(updateNameTransactionData);
}
this.updateNameTransactions = updateNameTransactions;
return sellNameTransactions;
}
private void fetchBuyNameTransactions(Repository repository) throws DataException {
private List<BuyNameTransactionData> fetchBuyNameTransactions() {
List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>();
// Fetch all the confirmed REGISTER_NAME transaction signatures
List<byte[]> buyNameSigs = repository.getTransactionRepository().getSignaturesMatchingCriteria(
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;
for (TransactionData transactionData : this.nameTransactions) {
if (transactionData.getType() == TransactionType.BUY_NAME) {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
buyNameTransactions.add(buyNameTransactionData);
}
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
buyNameTransactions.add(buyNameTransactionData);
}
this.buyNameTransactions = buyNameTransactions;
return buyNameTransactions;
}
private List<UpdateNameTransactionData> fetchUpdateTransactionsInvolvingName(String registeredName) {
List<UpdateNameTransactionData> matchedTransactions = new ArrayList<>();
private void fetchAllNameTransactions(Repository repository) throws DataException {
List<TransactionData> nameTransactions = new ArrayList<>();
for (UpdateNameTransactionData updateNameTransactionData : this.updateNameTransactions) {
if (Objects.equals(updateNameTransactionData.getName(), registeredName) ||
Objects.equals(updateNameTransactionData.getNewName(), registeredName)) {
// Fetch all the confirmed REGISTER_NAME transaction signatures
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(
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);
}
return matchedTransactions;
this.nameTransactions = nameTransactions;
}
private List<BuyNameTransactionData> fetchBuyTransactionsInvolvingName(String registeredName) {
List<BuyNameTransactionData> matchedTransactions = new ArrayList<>();
private List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
List<TransactionData> transactions = new ArrayList<>();
String reducedName = Unicode.sanitize(name);
// Fetch all the confirmed name-modification transactions
if (this.nameTransactions.isEmpty()) {
this.fetchAllNameTransactions(repository);
}
for (BuyNameTransactionData buyNameTransactionData : this.buyNameTransactions) {
if (Objects.equals(buyNameTransactionData.getName(), registeredName)) {
for (TransactionData transactionData : this.nameTransactions) {
matchedTransactions.add(buyNameTransactionData);
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 TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName) {
List<TransactionData> latestTransactions = new ArrayList<>();
List<UpdateNameTransactionData> updates = this.fetchUpdateTransactionsInvolvingName(registeredName);
List<BuyNameTransactionData> buys = this.fetchBuyTransactionsInvolvingName(registeredName);
private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName, Repository repository) throws DataException {
List<TransactionData> transactionsInvolvingName = this.fetchAllTransactionsInvolvingName(registeredName, repository);
// Get the latest updates for this name
UpdateNameTransactionData latestUpdateToName = updates.stream()
.filter(update -> update.getNewName().equals(registeredName))
.max(Comparator.comparing(UpdateNameTransactionData::getTimestamp))
// 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);
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);
}
return latestUpdateToName;
}
// 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);
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);
}
// Get the latest name-related transaction of any type
TransactionData latestUpdate = latestTransactions.stream()
.max(Comparator.comparing(TransactionData::getTimestamp))
.orElse(null);
for (TransactionData transactionData : this.nameTransactions) {
return latestUpdate;
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;
}
}

4
src/main/java/org/qortal/naming/Name.java

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

5
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();

5
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();

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

5
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.

5
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

12
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

5
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

5
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

5
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

5
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

5
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

5
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

5
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

5
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

5
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);

5
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());

5
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

5
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

5
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

5
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

5
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

5
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

5
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

5
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

5
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.

5
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.

5
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

5
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

12
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

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

5
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();

12
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

5
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();

10
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.
* <p>

5
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.

5
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();

5
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

5
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

18
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

5
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();

345
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);
}
}
}
Loading…
Cancel
Save