diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java
index e82ab14e..0a5246af 100644
--- a/src/main/java/org/qortal/at/AT.java
+++ b/src/main/java/org/qortal/at/AT.java
@@ -1,5 +1,7 @@
package org.qortal.at;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import org.ciyam.at.MachineState;
@@ -56,12 +58,12 @@ public class AT {
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
- machineState.isFrozen(), machineState.getFrozenBalance());
+ machineState.isFrozen(), machineState.getFrozenBalance(), null);
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
- this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
+ this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true, null);
}
// Getters / setters
@@ -84,13 +86,27 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress());
}
+ /**
+ * Potentially execute AT.
+ *
+ * Note that sleep-until-message support might set/reset
+ * sleep-related flags/values.
+ *
+ * {@link #getATStateData()} will return null if nothing happened.
+ *
+ * @param blockHeight
+ * @param blockTimestamp
+ * @return AT-generated transactions, possibly empty
+ * @throws DataException
+ */
public List run(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
- byte[] codeBytes = this.atData.getCodeBytes();
+ if (!api.willExecute(blockHeight))
+ return Collections.emptyList();
// Fetch latest ATStateData for this AT
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
@@ -100,8 +116,10 @@ public class AT {
throw new IllegalStateException("No previous AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
+ byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
try {
+ api.preExecute(state);
state.execute();
} catch (Exception e) {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
@@ -109,9 +127,16 @@ public class AT {
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
- long atFees = api.calcFinalFees(state);
- this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
+ // Nothing happened?
+ if (state.getSteps() == 0 && Arrays.equals(stateHash, latestAtStateData.getStateHash()))
+ // this.atStateData will be null
+ return Collections.emptyList();
+
+ long atFees = api.calcFinalFees(state);
+ Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
+
+ this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false, sleepUntilMessageTimestamp);
return api.getTransactions();
}
@@ -130,6 +155,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
+
+ // Special sleep-until-message support
+ this.atData.setSleepUntilMessageTimestamp(this.atStateData.getSleepUntilMessageTimestamp());
+
this.repository.getATRepository().save(this.atData);
}
@@ -157,6 +186,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
+
+ // Special sleep-until-message support
+ this.atData.setSleepUntilMessageTimestamp(previousStateData.getSleepUntilMessageTimestamp());
+
this.repository.getATRepository().save(this.atData);
}
diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java
index 582b44e2..d70ac9ba 100644
--- a/src/main/java/org/qortal/at/QortalATAPI.java
+++ b/src/main/java/org/qortal/at/QortalATAPI.java
@@ -32,6 +32,7 @@ import org.qortal.group.Group;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
+import org.qortal.repository.ATRepository.NextTransactionInfo;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
@@ -74,8 +75,46 @@ public class QortalATAPI extends API {
return this.transactions;
}
- public long calcFinalFees(MachineState state) {
- return state.getSteps() * this.ciyamAtSettings.feePerStep;
+ public boolean willExecute(int blockHeight) throws DataException {
+ // Sleep-until-message/height checking
+ Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
+
+ if (sleepUntilMessageTimestamp != null) {
+ // Quicker to check height, if sleep-until-height also active
+ Integer sleepUntilHeight = this.atData.getSleepUntilHeight();
+
+ boolean wakeDueToHeight = sleepUntilHeight != null && blockHeight >= sleepUntilHeight;
+
+ 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);
+
+ wakeDueToMessage = nextTransactionInfo != null;
+ }
+
+ // Can we skip?
+ if (!wakeDueToHeight && !wakeDueToMessage)
+ // this.atStateData will be null
+ return false;
+ }
+
+ return true;
+ }
+
+ public void preExecute(MachineState state) {
+ // Sleep-until-message/height checking
+ Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
+
+ if (sleepUntilMessageTimestamp != null) {
+ // We've passed checks, so clear sleep-related flags/values
+ this.setIsSleeping(state, false);
+ this.setSleepUntilHeight(state, null);
+ this.atData.setSleepUntilMessageTimestamp(null);
+ }
}
// Inherited methods from CIYAM AT API
@@ -408,6 +447,10 @@ public class QortalATAPI extends API {
// Utility methods
+ public long calcFinalFees(MachineState state) {
+ return state.getSteps() * this.ciyamAtSettings.feePerStep;
+ }
+
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32);
@@ -456,6 +499,15 @@ public class QortalATAPI extends API {
}
}
+ /*package*/ void sleepUntilMessageOrHeight(MachineState state, long txTimestamp, Long sleepUntilHeight) {
+ this.setIsSleeping(state, true);
+
+ this.atData.setSleepUntilMessageTimestamp(txTimestamp);
+
+ if (sleepUntilHeight != null)
+ this.setSleepUntilHeight(state, new Timestamp(sleepUntilHeight).blockHeight);
+ }
+
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java
index 67ab5b98..eb407450 100644
--- a/src/main/java/org/qortal/at/QortalFunctionCode.java
+++ b/src/main/java/org/qortal/at/QortalFunctionCode.java
@@ -84,6 +84,43 @@ public enum QortalFunctionCode {
api.setB(state, bBytes);
}
},
+ /**
+ * Sleep AT until a new message arrives after 'tx-timestamp'.
+ * 0x0503 tx-timestamp
+ */
+ SLEEP_UNTIL_MESSAGE(0x0503, 1, false) {
+ @Override
+ protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
+ if (functionData.value1 <= 0)
+ return;
+
+ long txTimestamp = functionData.value1;
+
+ QortalATAPI api = (QortalATAPI) state.getAPI();
+ api.sleepUntilMessageOrHeight(state, txTimestamp, null);
+ }
+ },
+ /**
+ * Sleep AT until a new message arrives, after 'tx-timestamp', or height reached.
+ * 0x0504 tx-timestamp height
+ */
+ SLEEP_UNTIL_MESSAGE_OR_HEIGHT(0x0504, 2, false) {
+ @Override
+ protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
+ if (functionData.value1 <= 0)
+ return;
+
+ long txTimestamp = functionData.value1;
+
+ if (functionData.value2 <= 0)
+ return;
+
+ long sleepUntilHeight = functionData.value2;
+
+ QortalATAPI api = (QortalATAPI) state.getAPI();
+ api.sleepUntilMessageOrHeight(state, txTimestamp, sleepUntilHeight);
+ }
+ },
/**
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
* 0x0510
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index b977a613..93441582 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -1246,12 +1246,13 @@ public class Block {
for (ATData atData : executableATs) {
AT at = new AT(this.repository, atData);
List atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp());
+ ATStateData atStateData = at.getATStateData();
+ // Didn't execute? (e.g. sleeping)
+ if (atStateData == null)
+ continue;
allAtTransactions.addAll(atTransactions);
-
- ATStateData atStateData = at.getATStateData();
this.ourAtStates.add(atStateData);
-
this.ourAtFees += atStateData.getFees();
}
diff --git a/src/main/java/org/qortal/data/at/ATData.java b/src/main/java/org/qortal/data/at/ATData.java
index 02f79f84..9e977acf 100644
--- a/src/main/java/org/qortal/data/at/ATData.java
+++ b/src/main/java/org/qortal/data/at/ATData.java
@@ -23,6 +23,7 @@ public class ATData {
private boolean isFrozen;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long frozenBalance;
+ private Long sleepUntilMessageTimestamp;
// Constructors
@@ -31,7 +32,8 @@ public class ATData {
}
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash,
- boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
+ boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance,
+ Long sleepUntilMessageTimestamp) {
this.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey;
this.creation = creation;
@@ -45,6 +47,7 @@ public class ATData {
this.hadFatalError = hadFatalError;
this.isFrozen = isFrozen;
this.frozenBalance = frozenBalance;
+ this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
/** For constructing skeleton ATData with bare minimum info. */
@@ -133,4 +136,12 @@ public class ATData {
this.frozenBalance = frozenBalance;
}
+ public Long getSleepUntilMessageTimestamp() {
+ return this.sleepUntilMessageTimestamp;
+ }
+
+ public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
+ this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
+ }
+
}
diff --git a/src/main/java/org/qortal/data/at/ATStateData.java b/src/main/java/org/qortal/data/at/ATStateData.java
index e689f5ae..ddace8e3 100644
--- a/src/main/java/org/qortal/data/at/ATStateData.java
+++ b/src/main/java/org/qortal/data/at/ATStateData.java
@@ -10,35 +10,32 @@ public class ATStateData {
private Long fees;
private boolean isInitial;
+ // Qortal-AT-specific
+ private Long sleepUntilMessageTimestamp;
+
// Constructors
/** Create new ATStateData */
- public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
+ public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees,
+ boolean isInitial, Long sleepUntilMessageTimestamp) {
this.ATAddress = ATAddress;
this.height = height;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
this.isInitial = isInitial;
+ this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
- this(ATAddress, height, null, stateHash, fees, isInitial);
- }
-
- /** For creating ATStateData from serialized bytes when we don't have all the info */
- public ATStateData(String ATAddress, byte[] stateHash) {
- // This won't ever be initial AT state from deployment as that's never serialized over the network,
- // but generated when the DeployAtTransaction is processed locally.
- this(ATAddress, null, null, stateHash, null, false);
+ this(ATAddress, height, null, stateHash, fees, isInitial, null);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
- // This won't ever be initial AT state from deployment as that's never serialized over the network,
- // but generated when the DeployAtTransaction is processed locally.
- this(ATAddress, null, null, stateHash, fees, false);
+ // This won't ever be initial AT state from deployment, as that's never serialized over the network.
+ this(ATAddress, null, null, stateHash, fees, false, null);
}
// Getters / setters
@@ -72,4 +69,12 @@ public class ATStateData {
return this.isInitial;
}
+ public Long getSleepUntilMessageTimestamp() {
+ return this.sleepUntilMessageTimestamp;
+ }
+
+ public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
+ this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
+ }
+
}
diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java
index b21a4909..9209b29e 100644
--- a/src/main/java/org/qortal/repository/ATRepository.java
+++ b/src/main/java/org/qortal/repository/ATRepository.java
@@ -78,7 +78,7 @@ public interface ATRepository {
/**
* Returns all ATStateData for a given block height.
*
- * Unlike getATState, only returns ATStateData saved at the given height.
+ * Unlike getATState, only returns partial ATStateData saved at the given height.
*
* @param height
* - block height
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
index f49da36d..cd7474ed 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
@@ -26,7 +26,7 @@ public class HSQLDBATRepository implements ATRepository {
public ATData fromATAddress(String atAddress) throws DataException {
String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
- + "is_frozen, frozen_balance "
+ + "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs "
+ "WHERE AT_address = ? LIMIT 1";
@@ -54,8 +54,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
+ Long sleepUntilMessageTimestamp = resultSet.getLong(12);
+ if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
+ sleepUntilMessageTimestamp = null;
+
return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
- isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
+ isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
+ sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
@@ -88,7 +93,7 @@ public class HSQLDBATRepository implements ATRepository {
public List getAllExecutableATs() throws DataException {
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, had_fatal_error, "
- + "is_frozen, frozen_balance "
+ + "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs "
+ "WHERE is_finished = false "
+ "ORDER BY created_when ASC";
@@ -122,8 +127,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
+ Long sleepUntilMessageTimestamp = resultSet.getLong(12);
+ if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
+ sleepUntilMessageTimestamp = null;
+
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
- isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
+ isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
+ sleepUntilMessageTimestamp);
executableATs.add(atData);
} while (resultSet.next());
@@ -141,7 +151,7 @@ public class HSQLDBATRepository implements ATRepository {
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
- .append("is_frozen, frozen_balance ")
+ .append("is_frozen, frozen_balance, sleep_until_message_timestamp ")
.append("FROM ATs ")
.append("WHERE code_hash = ? ");
bindParams.add(codeHash);
@@ -185,8 +195,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
+ Long sleepUntilMessageTimestamp = resultSet.getLong(13);
+ if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
+ sleepUntilMessageTimestamp = null;
+
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
- isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
+ isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
+ sleepUntilMessageTimestamp);
matchingATs.add(atData);
} while (resultSet.next());
@@ -225,7 +240,7 @@ public class HSQLDBATRepository implements ATRepository {
.bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash())
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
.bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
- .bind("frozen_balance", atData.getFrozenBalance());
+ .bind("frozen_balance", atData.getFrozenBalance()).bind("sleep_until_message_timestamp", atData.getSleepUntilMessageTimestamp());
try {
saveHelper.execute(this.repository);
@@ -248,7 +263,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
- String sql = "SELECT state_data, state_hash, fees, is_initial "
+ String sql = "SELECT state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates "
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
@@ -263,7 +278,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(3);
boolean isInitial = resultSet.getBoolean(4);
- return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
+ Long sleepUntilMessageTimestamp = resultSet.getLong(5);
+ if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
+ sleepUntilMessageTimestamp = null;
+
+ return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e);
}
@@ -271,7 +290,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getLatestATState(String atAddress) throws DataException {
- String sql = "SELECT height, state_data, state_hash, fees, is_initial "
+ String sql = "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? "
@@ -290,7 +309,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(4);
boolean isInitial = resultSet.getBoolean(5);
- return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
+ Long sleepUntilMessageTimestamp = resultSet.getLong(6);
+ if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
+ sleepUntilMessageTimestamp = null;
+
+ return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e);
}
@@ -303,10 +326,10 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024);
List