diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 7372a7c9..0423f855 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -111,6 +111,12 @@ public class Settings { private int maxUnconfirmedPerAccount = 25; /** Max milliseconds into future for accepting new, unconfirmed transactions */ private int maxTransactionTimestampFuture = 30 * 60 * 1000; // milliseconds + + /** Maximum number of CHAT transactions allowed per account in recent timeframe */ + private int maxRecentChatMessagesPerAccount = 250; + /** Maximum age of a CHAT transaction to be considered 'recent' */ + private long recentChatMessagesMaxAge = 60 * 60 * 1000L; // milliseconds + /** Whether we check, fetch and install auto-updates */ private boolean autoUpdateEnabled = true; /** How long between repository backups (ms), or 0 if disabled. */ @@ -640,6 +646,14 @@ public class Settings { return this.maxTransactionTimestampFuture; } + public int getMaxRecentChatMessagesPerAccount() { + return this.maxRecentChatMessagesPerAccount; + } + + public long getRecentChatMessagesMaxAge() { + return recentChatMessagesMaxAge; + } + public int getBlockCacheSize() { return this.blockCacheSize; } diff --git a/src/main/java/org/qortal/transaction/ChatTransaction.java b/src/main/java/org/qortal/transaction/ChatTransaction.java index b4ae9f37..72fea7a1 100644 --- a/src/main/java/org/qortal/transaction/ChatTransaction.java +++ b/src/main/java/org/qortal/transaction/ChatTransaction.java @@ -1,7 +1,9 @@ package org.qortal.transaction; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; @@ -16,6 +18,7 @@ import org.qortal.list.ResourceListManager; import org.qortal.repository.DataException; import org.qortal.repository.GroupRepository; import org.qortal.repository.Repository; +import org.qortal.settings.Settings; import org.qortal.transform.TransformationException; import org.qortal.transform.transaction.ChatTransactionTransformer; import org.qortal.transform.transaction.TransactionTransformer; @@ -169,6 +172,14 @@ public class ChatTransaction extends Transaction { } } + PublicKeyAccount creator = this.getCreator(); + if (creator == null) + return ValidationResult.MISSING_CREATOR; + + // Reject if unconfirmed pile already has X recent CHAT transactions from same creator + if (countRecentChatTransactionsByCreator(creator) >= Settings.getInstance().getMaxRecentChatMessagesPerAccount()) + return ValidationResult.TOO_MANY_UNCONFIRMED; + // If we exist in the repository then we've been imported as unconfirmed, // but we don't want to make it into a block, so return fake non-OK result. if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature())) @@ -219,6 +230,26 @@ public class ChatTransaction extends Transaction { return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce); } + private int countRecentChatTransactionsByCreator(PublicKeyAccount creator) throws DataException { + List unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(); + final Long now = NTP.getTime(); + long recentThreshold = Settings.getInstance().getRecentChatMessagesMaxAge(); + + // We only care about chat transactions, and only those that are considered 'recent' + Predicate hasSameCreatorAndIsRecentChat = transactionData -> { + if (transactionData.getType() != TransactionType.CHAT) + return false; + + if (transactionData.getTimestamp() < now - recentThreshold) + return false; + + return Arrays.equals(creator.getPublicKey(), transactionData.getCreatorPublicKey()); + }; + + return (int) unconfirmedTransactions.stream().filter(hasSameCreatorAndIsRecentChat).count(); + } + + /** * Ensure there's at least a skeleton account so people * can retrieve sender's public key using address, even if all their messages