From 7a40c3526f3b9dadd59517c165e4e479d04990dd Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 10 Nov 2020 15:30:54 +0000 Subject: [PATCH] Bugfixes and tests for SLEEP_UNTIL_MESSAGE --- src/main/java/org/qortal/at/AT.java | 1 + src/main/java/org/qortal/at/QortalATAPI.java | 3 +- .../repository/hsqldb/HSQLDBATRepository.java | 4 +- .../at/SleepUntilMessageOrHeightTests.java | 365 ++++++++++++++++++ .../test/at/SleepUntilMessageTests.java | 311 +++++++++++++++ 5 files changed, 680 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/qortal/test/at/SleepUntilMessageOrHeightTests.java create mode 100644 src/test/java/org/qortal/test/at/SleepUntilMessageTests.java diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java index 0a5246af..99ae57d5 100644 --- a/src/main/java/org/qortal/at/AT.java +++ b/src/main/java/org/qortal/at/AT.java @@ -106,6 +106,7 @@ public class AT { QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); if (!api.willExecute(blockHeight)) + // this.atStateData will be null return Collections.emptyList(); // Fetch latest ATStateData for this AT diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index d70ac9ba..bb8942cb 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -98,7 +98,6 @@ public class QortalATAPI extends API { // Can we skip? if (!wakeDueToHeight && !wakeDueToMessage) - // this.atStateData will be null return false; } @@ -505,7 +504,7 @@ public class QortalATAPI extends API { this.atData.setSleepUntilMessageTimestamp(txTimestamp); if (sleepUntilHeight != null) - this.setSleepUntilHeight(state, new Timestamp(sleepUntilHeight).blockHeight); + this.setSleepUntilHeight(state, sleepUntilHeight.intValue()); } /** Returns AT's account */ diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index cd7474ed..e45e4794 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -54,7 +54,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - Long sleepUntilMessageTimestamp = resultSet.getLong(12); + Long sleepUntilMessageTimestamp = resultSet.getLong(13); if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull()) sleepUntilMessageTimestamp = null; @@ -127,7 +127,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - Long sleepUntilMessageTimestamp = resultSet.getLong(12); + Long sleepUntilMessageTimestamp = resultSet.getLong(13); if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull()) sleepUntilMessageTimestamp = null; diff --git a/src/test/java/org/qortal/test/at/SleepUntilMessageOrHeightTests.java b/src/test/java/org/qortal/test/at/SleepUntilMessageOrHeightTests.java new file mode 100644 index 00000000..7ac952d2 --- /dev/null +++ b/src/test/java/org/qortal/test/at/SleepUntilMessageOrHeightTests.java @@ -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 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); + } + +} diff --git a/src/test/java/org/qortal/test/at/SleepUntilMessageTests.java b/src/test/java/org/qortal/test/at/SleepUntilMessageTests.java new file mode 100644 index 00000000..290f973a --- /dev/null +++ b/src/test/java/org/qortal/test/at/SleepUntilMessageTests.java @@ -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 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); + } + +}