forked from Qortal-Forker/qortal
		
	Merge branch 'AT-sleep-until-message'
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -15,8 +15,8 @@
 | 
			
		||||
/settings.json
 | 
			
		||||
/testnet*
 | 
			
		||||
/settings*.json
 | 
			
		||||
/testchain.json
 | 
			
		||||
/run-testnet.sh
 | 
			
		||||
/testchain*.json
 | 
			
		||||
/run-testnet*.sh
 | 
			
		||||
/.idea
 | 
			
		||||
/qortal.iml
 | 
			
		||||
.DS_Store
 | 
			
		||||
 
 | 
			
		||||
@@ -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,28 @@ public class AT {
 | 
			
		||||
		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 {
 | 
			
		||||
		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))
 | 
			
		||||
			// this.atStateData will be null
 | 
			
		||||
			return Collections.emptyList();
 | 
			
		||||
 | 
			
		||||
		// Fetch latest ATStateData for this AT
 | 
			
		||||
		ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
 | 
			
		||||
@@ -100,8 +117,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 +128,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 +156,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 +187,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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,45 @@ 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 && 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
 | 
			
		||||
@@ -412,6 +450,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);
 | 
			
		||||
@@ -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 */
 | 
			
		||||
	/* package */ Account getATAccount() {
 | 
			
		||||
		return new Account(this.repository, this.atData.getATAddress());
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,43 @@ public enum QortalFunctionCode {
 | 
			
		||||
			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>
 | 
			
		||||
	 * <tt>0x0510</tt>
 | 
			
		||||
 
 | 
			
		||||
@@ -1247,12 +1247,13 @@ public class Block {
 | 
			
		||||
		for (ATData atData : executableATs) {
 | 
			
		||||
			AT at = new AT(this.repository, atData);
 | 
			
		||||
			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);
 | 
			
		||||
 | 
			
		||||
			ATStateData atStateData = at.getATStateData();
 | 
			
		||||
			this.ourAtStates.add(atStateData);
 | 
			
		||||
 | 
			
		||||
			this.ourAtFees += atStateData.getFees();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ public interface ATRepository {
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns all ATStateData for a given block height.
 | 
			
		||||
	 * <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
 | 
			
		||||
	 *            - block height
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,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";
 | 
			
		||||
 | 
			
		||||
@@ -60,8 +60,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;
 | 
			
		||||
 | 
			
		||||
			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);
 | 
			
		||||
		}
 | 
			
		||||
@@ -94,7 +99,7 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
	public List<ATData> 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";
 | 
			
		||||
@@ -128,8 +133,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);
 | 
			
		||||
 | 
			
		||||
				executableATs.add(atData);
 | 
			
		||||
			} 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, ")
 | 
			
		||||
				.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);
 | 
			
		||||
@@ -191,8 +201,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());
 | 
			
		||||
@@ -210,7 +225,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, code_hash ")
 | 
			
		||||
				.append("is_frozen, frozen_balance, code_hash, sleep_until_message_timestamp ")
 | 
			
		||||
				.append("FROM ");
 | 
			
		||||
 | 
			
		||||
		// (VALUES (?), (?), ...) AS ATCodeHashes (code_hash)
 | 
			
		||||
@@ -264,9 +279,10 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
					frozenBalance = null;
 | 
			
		||||
 | 
			
		||||
				byte[] codeHash = resultSet.getBytes(13);
 | 
			
		||||
				Long sleepUntilMessageTimestamp = resultSet.getLong(14);
 | 
			
		||||
 | 
			
		||||
				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());
 | 
			
		||||
@@ -305,7 +321,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);
 | 
			
		||||
@@ -328,7 +344,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 = ? "
 | 
			
		||||
@@ -343,7 +359,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);
 | 
			
		||||
		}
 | 
			
		||||
@@ -351,7 +371,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 = ? "
 | 
			
		||||
@@ -370,7 +390,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);
 | 
			
		||||
		}
 | 
			
		||||
@@ -383,10 +407,10 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
		StringBuilder sql = new StringBuilder(1024);
 | 
			
		||||
		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 "
 | 
			
		||||
				+ "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 "
 | 
			
		||||
					+ "JOIN ATStatesData USING (AT_address, height) "
 | 
			
		||||
					+ "WHERE ATStates.AT_address = ATs.AT_address ");
 | 
			
		||||
@@ -440,7 +464,11 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
				long fees = resultSet.getLong(5);
 | 
			
		||||
				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);
 | 
			
		||||
			} while (resultSet.next());
 | 
			
		||||
@@ -471,7 +499,7 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
		StringBuilder sql = new StringBuilder(1024);
 | 
			
		||||
		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 "
 | 
			
		||||
				+ "CROSS JOIN LATERAL("
 | 
			
		||||
					+ "SELECT height, state_data, state_hash, fees, is_initial "
 | 
			
		||||
@@ -526,8 +554,10 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
				byte[] stateHash = resultSet.getBytes(4);
 | 
			
		||||
				long fees = resultSet.getLong(5);
 | 
			
		||||
				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);
 | 
			
		||||
			} while (resultSet.next());
 | 
			
		||||
@@ -662,7 +692,8 @@ public class HSQLDBATRepository implements ATRepository {
 | 
			
		||||
 | 
			
		||||
		atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
 | 
			
		||||
				.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 {
 | 
			
		||||
			atStatesSaver.execute(this.repository);
 | 
			
		||||
 
 | 
			
		||||
@@ -699,7 +699,7 @@ public class HSQLDBDatabaseUpdates {
 | 
			
		||||
					stmt.execute("CHECKPOINT");
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case 30:
 | 
			
		||||
				case 30: {
 | 
			
		||||
					// Split AT state data off to new table for better performance/management.
 | 
			
		||||
 | 
			
		||||
					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("CHECKPOINT");
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 31:
 | 
			
		||||
					// 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 + ")");
 | 
			
		||||
					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:
 | 
			
		||||
					// nothing to do
 | 
			
		||||
					return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -354,7 +354,8 @@ public class AtRepositoryTests extends Common {
 | 
			
		||||
					/*StateData*/ null,
 | 
			
		||||
					atStateData.getStateHash(),
 | 
			
		||||
					atStateData.getFees(),
 | 
			
		||||
					atStateData.isInitial());
 | 
			
		||||
					atStateData.isInitial(),
 | 
			
		||||
					atStateData.getSleepUntilMessageTimestamp());
 | 
			
		||||
			repository.getATRepository().save(newAtStateData);
 | 
			
		||||
 | 
			
		||||
			atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										311
									
								
								src/test/java/org/qortal/test/at/SleepUntilMessageTests.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								src/test/java/org/qortal/test/at/SleepUntilMessageTests.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user