Merge branch 'AT-sleep-until-message'

This commit is contained in:
CalDescent 2021-08-04 19:00:24 +01:00
commit ec008b4a16
13 changed files with 930 additions and 47 deletions

4
.gitignore vendored
View File

@ -15,8 +15,8 @@
/settings.json /settings.json
/testnet* /testnet*
/settings*.json /settings*.json
/testchain.json /testchain*.json
/run-testnet.sh /run-testnet*.sh
/.idea /.idea
/qortal.iml /qortal.iml
.DS_Store .DS_Store

View File

@ -1,5 +1,7 @@
package org.qortal.at; package org.qortal.at;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
@ -56,12 +58,12 @@ public class AT {
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash, this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(), machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance()); machineState.isFrozen(), machineState.getFrozenBalance(), null);
byte[] stateData = machineState.toBytes(); byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData); 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 // Getters / setters
@ -84,13 +86,28 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress()); this.repository.getATRepository().delete(this.atData.getATAddress());
} }
/**
* Potentially execute AT.
* <p>
* Note that sleep-until-message support might set/reset
* sleep-related flags/values.
* <p>
* {@link #getATStateData()} will return null if nothing happened.
* <p>
* @param blockHeight
* @param blockTimestamp
* @return AT-generated transactions, possibly empty
* @throws DataException
*/
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException { public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress(); String atAddress = this.atData.getATAddress();
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp); QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes(); if (!api.willExecute(blockHeight))
// this.atStateData will be null
return Collections.emptyList();
// Fetch latest ATStateData for this AT // Fetch latest ATStateData for this AT
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress); ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
@ -100,8 +117,10 @@ public class AT {
throw new IllegalStateException("No previous AT state data found"); throw new IllegalStateException("No previous AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable // [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); MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
try { try {
api.preExecute(state);
state.execute(); state.execute();
} catch (Exception e) { } catch (Exception e) {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e); throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
@ -109,9 +128,16 @@ public class AT {
byte[] stateData = state.toBytes(); byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData); 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(); return api.getTransactions();
} }
@ -130,6 +156,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError()); this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen()); this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance()); this.atData.setFrozenBalance(state.getFrozenBalance());
// Special sleep-until-message support
this.atData.setSleepUntilMessageTimestamp(this.atStateData.getSleepUntilMessageTimestamp());
this.repository.getATRepository().save(this.atData); this.repository.getATRepository().save(this.atData);
} }
@ -157,6 +187,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError()); this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen()); this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance()); this.atData.setFrozenBalance(state.getFrozenBalance());
// Special sleep-until-message support
this.atData.setSleepUntilMessageTimestamp(previousStateData.getSleepUntilMessageTimestamp());
this.repository.getATRepository().save(this.atData); this.repository.getATRepository().save(this.atData);
} }

View File

@ -32,6 +32,7 @@ import org.qortal.group.Group;
import org.qortal.repository.ATRepository; import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.repository.ATRepository.NextTransactionInfo;
import org.qortal.transaction.AtTransaction; import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
@ -74,8 +75,45 @@ public class QortalATAPI extends API {
return this.transactions; return this.transactions;
} }
public long calcFinalFees(MachineState state) { public boolean willExecute(int blockHeight) throws DataException {
return state.getSteps() * this.ciyamAtSettings.feePerStep; // 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 && sleepUntilHeight != 0 && 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)
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, 0);
this.atData.setSleepUntilMessageTimestamp(null);
}
} }
// Inherited methods from CIYAM AT API // Inherited methods from CIYAM AT API
@ -412,6 +450,10 @@ public class QortalATAPI extends API {
// Utility methods // 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. */ /** 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) { public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32); return Arrays.copyOfRange(fullSignature, 8, 32);
@ -460,6 +502,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, sleepUntilHeight.intValue());
}
/** Returns AT's account */ /** Returns AT's account */
/* package */ Account getATAccount() { /* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress()); return new Account(this.repository, this.atData.getATAddress());

View File

@ -84,6 +84,43 @@ public enum QortalFunctionCode {
api.setB(state, bBytes); api.setB(state, bBytes);
} }
}, },
/**
* Sleep AT until a new message arrives after 'tx-timestamp'.<br>
* <tt>0x0503 tx-timestamp</tt>
*/
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.<br>
* <tt>0x0504 tx-timestamp height</tt>
*/
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.<br> * Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
* <tt>0x0510</tt> * <tt>0x0510</tt>

View File

@ -1247,12 +1247,13 @@ public class Block {
for (ATData atData : executableATs) { for (ATData atData : executableATs) {
AT at = new AT(this.repository, atData); AT at = new AT(this.repository, atData);
List<AtTransaction> atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp()); List<AtTransaction> 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); allAtTransactions.addAll(atTransactions);
ATStateData atStateData = at.getATStateData();
this.ourAtStates.add(atStateData); this.ourAtStates.add(atStateData);
this.ourAtFees += atStateData.getFees(); this.ourAtFees += atStateData.getFees();
} }

View File

@ -23,6 +23,7 @@ public class ATData {
private boolean isFrozen; private boolean isFrozen;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long frozenBalance; private Long frozenBalance;
private Long sleepUntilMessageTimestamp;
// Constructors // Constructors
@ -31,7 +32,8 @@ public class ATData {
} }
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash, 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.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey; this.creatorPublicKey = creatorPublicKey;
this.creation = creation; this.creation = creation;
@ -45,6 +47,7 @@ public class ATData {
this.hadFatalError = hadFatalError; this.hadFatalError = hadFatalError;
this.isFrozen = isFrozen; this.isFrozen = isFrozen;
this.frozenBalance = frozenBalance; this.frozenBalance = frozenBalance;
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
} }
/** For constructing skeleton ATData with bare minimum info. */ /** For constructing skeleton ATData with bare minimum info. */
@ -133,4 +136,12 @@ public class ATData {
this.frozenBalance = frozenBalance; this.frozenBalance = frozenBalance;
} }
public Long getSleepUntilMessageTimestamp() {
return this.sleepUntilMessageTimestamp;
}
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
} }

View File

@ -10,35 +10,32 @@ public class ATStateData {
private Long fees; private Long fees;
private boolean isInitial; private boolean isInitial;
// Qortal-AT-specific
private Long sleepUntilMessageTimestamp;
// Constructors // Constructors
/** Create new ATStateData */ /** 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.ATAddress = ATAddress;
this.height = height; this.height = height;
this.stateData = stateData; this.stateData = stateData;
this.stateHash = stateHash; this.stateHash = stateHash;
this.fees = fees; this.fees = fees;
this.isInitial = isInitial; this.isInitial = isInitial;
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
} }
/** For recreating per-block ATStateData from repository where not all info is needed */ /** 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) { public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
this(ATAddress, height, null, stateHash, fees, isInitial); 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) {
// 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);
} }
/** For creating ATStateData from serialized bytes when we don't have all the info */ /** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, Long fees) { 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, // 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, null);
this(ATAddress, null, null, stateHash, fees, false);
} }
// Getters / setters // Getters / setters
@ -72,4 +69,12 @@ public class ATStateData {
return this.isInitial; return this.isInitial;
} }
public Long getSleepUntilMessageTimestamp() {
return this.sleepUntilMessageTimestamp;
}
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
} }

View File

@ -103,7 +103,7 @@ public interface ATRepository {
/** /**
* Returns all ATStateData for a given block height. * Returns all ATStateData for a given block height.
* <p> * <p>
* Unlike <tt>getATState</tt>, only returns ATStateData saved at the given height. * Unlike <tt>getATState</tt>, only returns <i>partial</i> ATStateData saved at the given height.
* *
* @param height * @param height
* - block height * - block height

View File

@ -32,7 +32,7 @@ public class HSQLDBATRepository implements ATRepository {
public ATData fromATAddress(String atAddress) throws DataException { public ATData fromATAddress(String atAddress) throws DataException {
String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, " String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, " + "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
+ "is_frozen, frozen_balance " + "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs " + "FROM ATs "
+ "WHERE AT_address = ? LIMIT 1"; + "WHERE AT_address = ? LIMIT 1";
@ -60,8 +60,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull()) if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null; frozenBalance = null;
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, 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) { } catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e); throw new DataException("Unable to fetch AT from repository", e);
} }
@ -94,7 +99,7 @@ public class HSQLDBATRepository implements ATRepository {
public List<ATData> getAllExecutableATs() throws DataException { public List<ATData> getAllExecutableATs() throws DataException {
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, " String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, had_fatal_error, " + "is_sleeping, sleep_until_height, had_fatal_error, "
+ "is_frozen, frozen_balance " + "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs " + "FROM ATs "
+ "WHERE is_finished = false " + "WHERE is_finished = false "
+ "ORDER BY created_when ASC"; + "ORDER BY created_when ASC";
@ -128,8 +133,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull()) if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null; 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, 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); executableATs.add(atData);
} while (resultSet.next()); } while (resultSet.next());
@ -147,7 +157,7 @@ public class HSQLDBATRepository implements ATRepository {
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ") 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_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("FROM ATs ")
.append("WHERE code_hash = ? "); .append("WHERE code_hash = ? ");
bindParams.add(codeHash); bindParams.add(codeHash);
@ -191,8 +201,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull()) if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null; 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, 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); matchingATs.add(atData);
} while (resultSet.next()); } while (resultSet.next());
@ -210,7 +225,7 @@ public class HSQLDBATRepository implements ATRepository {
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ") 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_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
.append("is_frozen, frozen_balance, code_hash ") .append("is_frozen, frozen_balance, code_hash, sleep_until_message_timestamp ")
.append("FROM "); .append("FROM ");
// (VALUES (?), (?), ...) AS ATCodeHashes (code_hash) // (VALUES (?), (?), ...) AS ATCodeHashes (code_hash)
@ -264,9 +279,10 @@ public class HSQLDBATRepository implements ATRepository {
frozenBalance = null; frozenBalance = null;
byte[] codeHash = resultSet.getBytes(13); byte[] codeHash = resultSet.getBytes(13);
Long sleepUntilMessageTimestamp = resultSet.getLong(14);
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, 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); matchingATs.add(atData);
} while (resultSet.next()); } while (resultSet.next());
@ -305,7 +321,7 @@ public class HSQLDBATRepository implements ATRepository {
.bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash()) .bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash())
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight()) .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("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 { try {
saveHelper.execute(this.repository); saveHelper.execute(this.repository);
@ -328,7 +344,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override @Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { 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 " + "FROM ATStates "
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) " + "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? " + "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
@ -343,7 +359,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(3); long fees = resultSet.getLong(3);
boolean isInitial = resultSet.getBoolean(4); 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) { } catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e); throw new DataException("Unable to fetch AT state from repository", e);
} }
@ -351,7 +371,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override @Override
public ATStateData getLatestATState(String atAddress) throws DataException { 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 " + "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) " + "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? " + "WHERE ATStates.AT_address = ? "
@ -370,7 +390,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(4); long fees = resultSet.getLong(4);
boolean isInitial = resultSet.getBoolean(5); 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) { } catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e); throw new DataException("Unable to fetch latest AT state from repository", e);
} }
@ -383,10 +407,10 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024); StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>(); List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial " sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, FinalATStates.sleep_until_message_timestamp "
+ "FROM ATs " + "FROM ATs "
+ "CROSS JOIN LATERAL(" + "CROSS JOIN LATERAL("
+ "SELECT height, state_data, state_hash, fees, is_initial " + "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates " + "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) " + "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ATs.AT_address "); + "WHERE ATStates.AT_address = ATs.AT_address ");
@ -440,7 +464,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(5); long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6); boolean isInitial = resultSet.getBoolean(6);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial); Long sleepUntilMessageTimestamp = resultSet.getLong(7);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
atStates.add(atStateData); atStates.add(atStateData);
} while (resultSet.next()); } while (resultSet.next());
@ -471,7 +499,7 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024); StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>(); List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial " sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATs " + "FROM ATs "
+ "CROSS JOIN LATERAL(" + "CROSS JOIN LATERAL("
+ "SELECT height, state_data, state_hash, fees, is_initial " + "SELECT height, state_data, state_hash, fees, is_initial "
@ -526,8 +554,10 @@ public class HSQLDBATRepository implements ATRepository {
byte[] stateHash = resultSet.getBytes(4); byte[] stateHash = resultSet.getBytes(4);
long fees = resultSet.getLong(5); long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6); boolean isInitial = resultSet.getBoolean(6);
Long sleepUntilMessageTimestamp = resultSet.getLong(7);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial); ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial,
sleepUntilMessageTimestamp);
atStates.add(atStateData); atStates.add(atStateData);
} while (resultSet.next()); } while (resultSet.next());
@ -662,7 +692,8 @@ public class HSQLDBATRepository implements ATRepository {
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_hash", atStateData.getStateHash()) .bind("state_hash", atStateData.getStateHash())
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial()); .bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial())
.bind("sleep_until_message_timestamp", atStateData.getSleepUntilMessageTimestamp());
try { try {
atStatesSaver.execute(this.repository); atStatesSaver.execute(this.repository);

View File

@ -699,7 +699,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT"); stmt.execute("CHECKPOINT");
break; break;
case 30: case 30: {
// Split AT state data off to new table for better performance/management. // Split AT state data off to new table for better performance/management.
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) { if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
@ -774,6 +774,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates"); stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT"); stmt.execute("CHECKPOINT");
break; break;
}
case 31: case 31:
// Fix latest AT state cache which was previous created as TEMPORARY // Fix latest AT state cache which was previous created as TEMPORARY
@ -822,6 +823,41 @@ public class HSQLDBDatabaseUpdates {
+ "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")"); + "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")");
break; break;
case 34: {
// AT sleep-until-message support
LOGGER.info("Altering AT table in repository - this might take a while... (approx. 20 seconds on high-spec)");
stmt.execute("ALTER TABLE ATs ADD sleep_until_message_timestamp BIGINT");
// Create new AT-states table with new column
stmt.execute("CREATE TABLE ATStatesNew ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, sleep_until_message_timestamp BIGINT, "
+ "PRIMARY KEY (AT_address, height), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
stmt.execute("CHECKPOINT");
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
final int heightStep = 100;
LOGGER.info("Altering AT states table in repository - this might take a while... (approx. 3 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesNew ("
+ "SELECT AT_address, height, state_hash, fees, is_initial, NULL "
+ "FROM ATStates "
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
stmt.execute("DROP TABLE ATStates");
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT");
break;
}
default: default:
// nothing to do // nothing to do
return false; return false;

View File

@ -354,7 +354,8 @@ public class AtRepositoryTests extends Common {
/*StateData*/ null, /*StateData*/ null,
atStateData.getStateHash(), atStateData.getStateHash(),
atStateData.getFees(), atStateData.getFees(),
atStateData.isInitial()); atStateData.isInitial(),
atStateData.getSleepUntilMessageTimestamp());
repository.getATRepository().save(newAtStateData); repository.getATRepository().save(newAtStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);

View File

@ -0,0 +1,365 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.utils.BitTwiddling;
public class SleepUntilMessageOrHeightTests extends Common {
private static final byte[] messageData = new byte[] { 0x44 };
private static final byte[] creationBytes = buildSleepUntilMessageOrHeightAT();
private static final long fundingAmount = 1_00000000L;
private static final long WAKE_HEIGHT = 10L;
private Repository repository = null;
private PrivateKeyAccount deployer;
private DeployAtTransaction deployAtTransaction;
private Account atAccount;
private String atAddress;
private byte[] rawNextTimestamp = new byte[32];
private Transaction transaction;
@Before
public void before() throws DataException {
Common.useDefaultSettings();
this.repository = RepositoryManager.getRepository();
this.deployer = Common.getTestAccount(repository, "alice");
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
this.atAccount = deployAtTransaction.getATAccount();
this.atAddress = deployAtTransaction.getATAccount().getAddress();
}
@After
public void after() throws DataException {
if (this.repository != null)
this.repository.close();
this.repository = null;
}
@Test
public void testDeploy() throws DataException {
// Confirm initial value is zero
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
@Test
public void testFeelessSleep() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint block
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testFeelessSleep2() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 0; i < 5; ++i)
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testSleepUntilMessage() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
BlockUtils.mintBlock(repository);
// Send message to AT
transaction = sendMessage(repository, deployer, messageData, atAddress);
BlockUtils.mintBlock(repository);
// Mint block so AT executes and finds message
BlockUtils.mintBlock(repository);
// Confirm AT finds message
assertTimestamp(repository, atAddress, transaction);
}
@Test
public void testSleepUntilHeight() throws DataException {
// AT deployment in block 2
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
BlockUtils.mintBlock(repository); // height now 3
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 3; i < WAKE_HEIGHT; ++i)
BlockUtils.mintBlock(repository);
// We should now be at WAKE_HEIGHT
long height = repository.getBlockRepository().getBlockchainHeight();
assertEquals(WAKE_HEIGHT, height);
// AT should have woken and run at this height so balance should have changed
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertNotSame(preMintBalance, postMintBalance);
// Confirm AT has no message
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
// Mint yet another block
BlockUtils.mintBlock(repository);
// AT should also have woken and run at this height so balance should have changed
// Fetch new AT balance
long postMint2Balance = atAccount.getConfirmedBalance(Asset.QORT);
assertNotSame(postMintBalance, postMint2Balance);
// Confirm AT still has no message
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
private static byte[] buildSleepUntilMessageOrHeightAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Beginning of data segment for easy extraction
final int addrNextTx = addrCounter;
addrCounter += 4;
final int addrNextTxIndex = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
final int addrWakeHeight = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// skip addrNextTx
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
// Store pointer to addrNextTx at addrNextTxIndex
dataByteBuffer.putLong(addrNextTx);
// skip addrLastTxTimestamp
dataByteBuffer.position(dataByteBuffer.position() + MachineState.VALUE_SIZE);
// Store fixed wake height (block 10)
dataByteBuffer.putLong(WAKE_HEIGHT);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE_OR_HEIGHT.value, addrLastTxTimestamp, addrWakeHeight));
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
// Stop if timestamp part of A is zero
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
// We're done
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
return messageTransaction;
}
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
int height = transaction.getHeight();
byte[] transactionSignature = transaction.getTransactionData().getSignature();
BlockData blockData = repository.getBlockRepository().fromHeight(height);
assertNotNull(blockData);
Block block = new Block(repository, blockData);
List<Transaction> blockTransactions = block.getTransactions();
int sequence;
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
break;
assertNotSame(-1, sequence);
byte[] rawNextTimestamp = new byte[32];
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
Timestamp expectedTimestamp = new Timestamp(height, sequence);
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
height, sequence,
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
),
expectedTimestamp.longValue(),
actualTimestamp.longValue());
byte[] expectedPartialSignature = new byte[24];
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
byte[] actualPartialSignature = new byte[24];
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
}
}

View File

@ -0,0 +1,311 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.utils.BitTwiddling;
public class SleepUntilMessageTests extends Common {
private static final byte[] messageData = new byte[] { 0x44 };
private static final byte[] creationBytes = buildSleepUntilMessageAT();
private static final long fundingAmount = 1_00000000L;
private Repository repository = null;
private PrivateKeyAccount deployer;
private DeployAtTransaction deployAtTransaction;
private Account atAccount;
private String atAddress;
private byte[] rawNextTimestamp = new byte[32];
private Transaction transaction;
@Before
public void before() throws DataException {
Common.useDefaultSettings();
this.repository = RepositoryManager.getRepository();
this.deployer = Common.getTestAccount(repository, "alice");
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
this.atAccount = deployAtTransaction.getATAccount();
this.atAddress = deployAtTransaction.getATAccount().getAddress();
}
@After
public void after() throws DataException {
if (this.repository != null)
this.repository.close();
this.repository = null;
}
@Test
public void testDeploy() throws DataException {
// Confirm initial value is zero
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
@Test
public void testFeelessSleep() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint block
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testFeelessSleep2() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testSleepUntilMessage() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Send message to AT
transaction = sendMessage(repository, deployer, messageData, atAddress);
BlockUtils.mintBlock(repository);
// Mint block so AT executes and finds message
BlockUtils.mintBlock(repository);
// Confirm AT finds message
assertTimestamp(repository, atAddress, transaction);
}
private static byte[] buildSleepUntilMessageAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Beginning of data segment for easy extraction
final int addrNextTx = addrCounter;
addrCounter += 4;
final int addrNextTxIndex = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// skip addrNextTx
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
// Store pointer to addrNextTx at addrNextTxIndex
dataByteBuffer.putLong(addrNextTx);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxTimestamp));
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
// Stop if timestamp part of A is zero
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
// We're done
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
return messageTransaction;
}
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
int height = transaction.getHeight();
byte[] transactionSignature = transaction.getTransactionData().getSignature();
BlockData blockData = repository.getBlockRepository().fromHeight(height);
assertNotNull(blockData);
Block block = new Block(repository, blockData);
List<Transaction> blockTransactions = block.getTransactions();
int sequence;
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
break;
assertNotSame(-1, sequence);
byte[] rawNextTimestamp = new byte[32];
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
Timestamp expectedTimestamp = new Timestamp(height, sequence);
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
height, sequence,
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
),
expectedTimestamp.longValue(),
actualTimestamp.longValue());
byte[] expectedPartialSignature = new byte[24];
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
byte[] actualPartialSignature = new byte[24];
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
}
}