diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java index 005bb0cd..cf5d85da 100644 --- a/src/main/java/org/qortal/at/AT.java +++ b/src/main/java/org/qortal/at/AT.java @@ -1,5 +1,6 @@ package org.qortal.at; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -10,6 +11,7 @@ import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.data.transaction.TransactionData; import org.qortal.repository.ATRepository; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -22,6 +24,8 @@ public class AT { private ATData atData; private ATStateData atStateData; + private List parentBlockTransactions = new ArrayList<>(); + // Constructors public AT(Repository repository, ATData atData, ATStateData atStateData) { @@ -72,6 +76,10 @@ public class AT { return this.atStateData; } + public void setParentBlockTransactions(List transactions) { + this.parentBlockTransactions = transactions; + } + // Processing public void deploy() throws DataException { @@ -105,7 +113,7 @@ public class AT { QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp); QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); - if (!api.willExecute(blockHeight)) + if (!api.willExecute(blockHeight, this.parentBlockTransactions)) // this.atStateData will be null return Collections.emptyList(); diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 829c391f..315581bd 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -3,6 +3,7 @@ package org.qortal.at; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,11 +24,7 @@ import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; -import org.qortal.data.transaction.ATTransactionData; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.MessageTransactionData; -import org.qortal.data.transaction.PaymentTransactionData; -import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.*; import org.qortal.group.Group; import org.qortal.repository.ATRepository; import org.qortal.repository.DataException; @@ -75,7 +72,7 @@ public class QortalATAPI extends API { return this.transactions; } - public boolean willExecute(int blockHeight) throws DataException { + public boolean willExecute(int blockHeight, List parentBlockTransactions) throws DataException { // Sleep-until-message/height checking Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp(); @@ -87,13 +84,27 @@ public class QortalATAPI extends API { boolean wakeDueToMessage = false; if (!wakeDueToHeight) { - // No avoiding asking repository - Timestamp previousTxTimestamp = new Timestamp(sleepUntilMessageTimestamp); - NextTransactionInfo nextTransactionInfo = this.repository.getATRepository().findNextTransaction(this.atData.getATAddress(), - previousTxTimestamp.blockHeight, - previousTxTimestamp.transactionSequence); + // Check parent block's transactions to see if any relate to this AT + for (TransactionData transactionData : parentBlockTransactions) { + if (this.wasTransactionSentToThisAT(transactionData)) { + wakeDueToMessage = true; + } + } - wakeDueToMessage = nextTransactionInfo != null; + if (wakeDueToMessage) { + // Double check with repository that this AT should be executed, to filter out cases such as TRANSFER_ASSET + Timestamp previousTxTimestamp = new Timestamp(sleepUntilMessageTimestamp); + NextTransactionInfo nextTransactionInfo = this.repository.getATRepository().findNextTransaction(this.atData.getATAddress(), + previousTxTimestamp.blockHeight, + previousTxTimestamp.transactionSequence); + + wakeDueToMessage = nextTransactionInfo != null; + } + else { + // No relevant transactions in previous block, so there is no need to check the db + // TODO: do we need to handle ATs that were previously frozen and have now recovered, or + // would we always detect a transaction in the last block in these cases? + } } // Can we skip? @@ -104,6 +115,32 @@ public class QortalATAPI extends API { return true; } + private boolean wasTransactionSentToThisAT(TransactionData transactionData) { + switch (transactionData.getType()) { + case PAYMENT: { + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; + return Objects.equals(paymentTransactionData.getRecipient(), this.atData.getATAddress()); + } + case TRANSFER_ASSET: { + // ATs don't check for TRANSFER_ASSET, but an AT's balance could be topped up using TRANSFER_ASSET, + // therefore unfreezing it. + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData; + return Objects.equals(transferAssetTransactionData.getRecipient(), this.atData.getATAddress()); + } + case MESSAGE: { + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + return Objects.equals(messageTransactionData.getRecipient(), this.atData.getATAddress()); + } + case AT: { + ATTransactionData atTransactionData = (ATTransactionData) transactionData; + return Objects.equals(atTransactionData.getRecipient(), this.atData.getATAddress()); + } + default: { + return false; + } + } + } + public void preExecute(MachineState state) { // Sleep-until-message/height checking Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp(); diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index e030e6a6..577425a0 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1378,6 +1378,8 @@ public class Block { * */ private void executeATs() throws DataException { + Long startTime = NTP.getTime(); + // We're expecting a lack of AT state data at this point. if (this.ourAtStates != null) throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists"); @@ -1391,9 +1393,13 @@ public class Block { // Find all executable ATs, ordered by earliest creation date first List executableATs = this.repository.getATRepository().getAllExecutableATs(); + // Get all transactions from the parent block. These are used to avoid unnecessary AT executions / db lookups. + List parentBlockTransactions = repository.getBlockRepository().getTransactionsFromSignature(this.blockData.getReference()); + // Run each AT, appends AT-Transactions and corresponding AT states, to our lists for (ATData atData : executableATs) { AT at = new AT(this.repository, atData); + at.setParentBlockTransactions(parentBlockTransactions); List atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp()); ATStateData atStateData = at.getATStateData(); // Didn't execute? (e.g. sleeping) @@ -1417,6 +1423,8 @@ public class Block { // AT Transactions do not affect block's transaction count // AT Transactions do not affect block's transaction signature + + LOGGER.info("Executing {} ATs in block {} took {} ms", executableATs.size(), this.blockData.getHeight(), (NTP.getTime()-startTime)); } /** Returns whether block's minter is actually allowed to mint this block. */