Compare commits

...

13 Commits

Author SHA1 Message Date
CalDescent
1d65e34fe5 Revert "Added DogecoinACCTv2 and DogecoinACCTv2TradeBot"
This reverts commit 797dff4752.
2021-08-04 18:59:36 +01:00
CalDescent
8ae78703ca Revert "Initial attempt at adding "sleep until message" functionality to DOGE ACCTv2."
This reverts commit a1c61a1146.
2021-08-04 18:59:30 +01:00
CalDescent
bd4b9a9fd3 Modified .gitignore to allow multiple testnets to exist by adding a number or other suffix. 2021-08-04 18:56:16 +01:00
CalDescent
961c5ea962 Treat zero as null in sleepUntilHeight AT data. This is needed because we are unable to call setSleepUntilHeight() with a null value due to the datatype used in the CIYAM AT library. An alternate option would be to fork the AT library and use an Integer or Long rather than an int, but since we don't have a block zero, this is still a valid thing to check even when using that approach. 2021-08-04 09:22:17 +01:00
CalDescent
a1c61a1146 Initial attempt at adding "sleep until message" functionality to DOGE ACCTv2. 2021-08-02 20:08:53 +01:00
CalDescent
797dff4752 Added DogecoinACCTv2 and DogecoinACCTv2TradeBot 2021-08-02 20:07:34 +01:00
CalDescent
68190c8c76 Fixed a build error relating to using an int rather than Integer in the CIYAM AT library. Solved for now by using 0 instead of null, but will review this again before release. 2021-08-01 18:07:19 +01:00
CalDescent
dde47bc1fc Fixed build errors by adding sleepUntilMessageTimestamp to recent method additions. 2021-08-01 18:05:58 +01:00
CalDescent
744deaed8d Fixed merge issue due to differing db schemas. 2021-08-01 10:39:34 +01:00
CalDescent
a62910c8b6 Merge branch 'master' into AT-sleep-until-message 2021-08-01 10:30:52 +01:00
catbref
a9c7142d7b Speed up AT states reshape 2020-11-10 16:46:07 +00:00
catbref
7a40c3526f Bugfixes and tests for SLEEP_UNTIL_MESSAGE 2020-11-10 15:44:47 +00:00
catbref
3253d9d3fb WIP: initial implementation of AT sleep-until-message (untested) 2020-11-10 15:44:47 +00:00
13 changed files with 930 additions and 47 deletions

4
.gitignore vendored
View File

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

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

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);
}
}