diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index b5779c65..0f7c28a2 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -250,7 +250,8 @@ public class HSQLDBATRepository implements ATRepository { public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { String sql = "SELECT state_data, state_hash, fees, is_initial " + "FROM ATStates " - + "WHERE AT_address = ? AND height = ? " + + "LEFT OUTER JOIN ATStatesData USING (AT_address, height) " + + "WHERE ATStates.AT_address = ? AND ATStates.height = ? " + "LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) { @@ -272,10 +273,11 @@ public class HSQLDBATRepository implements ATRepository { public ATStateData getLatestATState(String atAddress) throws DataException { String sql = "SELECT height, state_data, state_hash, fees, is_initial " + "FROM ATStates " - + "WHERE AT_address = ? " - // AT_address then height so the compound primary key is used as an index - // Both must be the same direction also - + "ORDER BY AT_address DESC, height DESC " + + "JOIN ATStatesData USING (AT_address, height) " + + "WHERE ATStates.AT_address = ? " + // Order by AT_address and height to use compound primary key as index + // Both must be the same direction (DESC) also + + "ORDER BY ATStates.AT_address DESC, ATStates.height DESC " + "LIMIT 1 "; try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) { @@ -306,16 +308,17 @@ public class HSQLDBATRepository implements ATRepository { + "CROSS JOIN LATERAL(" + "SELECT height, state_data, state_hash, fees, is_initial " + "FROM ATStates " + + "JOIN ATStatesData USING (AT_address, height) " + "WHERE ATStates.AT_address = ATs.AT_address "); if (minimumFinalHeight != null) { - sql.append("AND height >= ? "); + sql.append("AND ATStates.height >= ? "); bindParams.add(minimumFinalHeight); } - // AT_address then height so the compound primary key is used as an index - // Both must be the same direction also - sql.append("ORDER BY AT_address DESC, height DESC " + // Order by AT_address and height to use compound primary key as index + // Both must be the same direction (DESC) also + sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC " + "LIMIT 1 " + ") AS FinalATStates " + "WHERE code_hash = ? "); @@ -337,7 +340,7 @@ public class HSQLDBATRepository implements ATRepository { bindParams.add(rawExpectedValue); } - sql.append(" ORDER BY height "); + sql.append(" ORDER BY FinalATStates.height "); if (reverse != null && reverse) sql.append("DESC"); @@ -431,7 +434,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public void prepareForAtStateTrimming() throws DataException { - // Rebuild cache of latest, non-finished AT states that we can't trim + // Rebuild cache of latest AT states that we can't trim String deleteSql = "DELETE FROM LatestATStates"; try { this.repository.executeCheckedUpdate(deleteSql); @@ -463,13 +466,12 @@ public class HSQLDBATRepository implements ATRepository { // We're often called so no need to trim all states in one go. // Limit updates to reduce CPU and memory load. - String sql = "UPDATE ATStates SET state_data = NULL " - + "WHERE state_data IS NOT NULL " - + "AND height BETWEEN ? AND ? " + String sql = "DELETE FROM ATStatesData " + + "WHERE height BETWEEN ? AND ? " + "AND NOT EXISTS(" + "SELECT TRUE FROM LatestATStates " - + "WHERE LatestATStates.AT_address = ATStates.AT_address " - + "AND LatestATStates.height = ATStates.height" + + "WHERE LatestATStates.AT_address = ATStatesData.AT_address " + + "AND LatestATStates.height = ATStatesData.height" + ") " + "LIMIT ?"; @@ -487,23 +489,44 @@ public class HSQLDBATRepository implements ATRepository { if (atStateData.getStateHash() == null || atStateData.getHeight() == null) throw new IllegalArgumentException("Refusing to save partial AT state into repository!"); - HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); + HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates"); - saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) - .bind("state_data", atStateData.getStateData()).bind("state_hash", atStateData.getStateHash()) + atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) + .bind("state_hash", atStateData.getStateHash()) .bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial()); try { - saveHelper.execute(this.repository); + atStatesSaver.execute(this.repository); } catch (SQLException e) { throw new DataException("Unable to save AT state into repository", e); } + + if (atStateData.getStateData() != null) { + HSQLDBSaver atStatesDataSaver = new HSQLDBSaver("ATStatesData"); + + atStatesDataSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) + .bind("state_data", atStateData.getStateData()); + + try { + atStatesDataSaver.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save AT state data into repository", e); + } + } else { + try { + this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", + atStateData.getATAddress(), atStateData.getHeight()); + } catch (SQLException e) { + throw new DataException("Unable to delete AT state data from repository", e); + } + } } @Override public void delete(String atAddress, int height) throws DataException { try { this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height); + this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height); } catch (SQLException e) { throw new DataException("Unable to delete AT state from repository", e); } @@ -513,6 +536,7 @@ public class HSQLDBATRepository implements ATRepository { public void deleteATStates(int height) throws DataException { try { this.repository.delete("ATStates", "height = ?", height); + this.repository.delete("ATStatesData", "height = ?", height); } catch (SQLException e) { throw new DataException("Unable to delete AT states from repository", e); } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 3255c045..72d54111 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -660,9 +660,10 @@ public class HSQLDBDatabaseUpdates { break; case 25: + // DISABLED: improved version in case 30! // Remove excess created_when from ATStates - stmt.execute("ALTER TABLE ATStates DROP created_when"); - stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)"); + // stmt.execute("ALTER TABLE ATStates DROP created_when"); + // stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)"); break; case 26: @@ -691,6 +692,69 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CHECKPOINT"); break; + case 30: + // Split AT state data off to new table for better performance/management. + + if (!"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) { + // First, backup node-local data in case user wants to avoid long reshape and use bootstrap instead + stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'"); + stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'"); + LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states"); + LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository"); + } + + // Create new AT-states table without full state data + 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, " + + "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("Rebuilding AT state summaries in repository - this might take a while... (approx. 2 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 " + + "FROM ATStates " + + "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1) + + ")"); + stmt.execute("COMMIT"); + } + stmt.execute("CHECKPOINT"); + + LOGGER.info("Rebuilding AT states height index in repository - this might take about 3x longer..."); + stmt.execute("CREATE INDEX ATStatesHeightIndex ON ATStatesNew (height)"); + stmt.execute("CHECKPOINT"); + + stmt.execute("CREATE TABLE ATStatesData (" + + "AT_address QortalAddress, height INTEGER NOT NULL, state_data ATState NOT NULL, " + + "PRIMARY KEY (height, AT_address), " + + "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); + stmt.execute("SET TABLE ATStatesData NEW SPACE"); + stmt.execute("CHECKPOINT"); + + LOGGER.info("Rebuilding AT state data in repository - this might take a while... (approx. 2 mins on high-spec)"); + for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) { + stmt.execute("INSERT INTO ATStatesData (" + + "SELECT AT_address, height, state_data " + + "FROM ATstates " + + "WHERE state_data IS NOT NULL " + + "AND 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; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index fc0ff259..d3815dcc 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -426,7 +426,7 @@ public class HSQLDBRepository implements Repository { } /** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */ - private static String getDbPathname(String connectionUrl) { + /*package*/ static String getDbPathname(String connectionUrl) { Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)"); Matcher matcher = pattern.matcher(connectionUrl); diff --git a/src/test/java/org/qortal/test/at/AtRepositoryTests.java b/src/test/java/org/qortal/test/at/AtRepositoryTests.java new file mode 100644 index 00000000..9d19f0eb --- /dev/null +++ b/src/test/java/org/qortal/test/at/AtRepositoryTests.java @@ -0,0 +1,426 @@ +package org.qortal.test.at; + +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.ciyam.at.CompilationException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +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; + +public class AtRepositoryTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetATStateAtHeightWithData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + Integer testHeight = 8; + ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testGetATStateAtHeightWithoutData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + int maxHeight = 8; + Integer testHeight = maxHeight - 2; + + // Trim AT state data + repository.getATRepository().prepareForAtStateTrimming(); + repository.getATRepository().trimAtStates(2, maxHeight, 1000); + + ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNull(atStateData.getStateData()); + } + } + + @Test + public void testGetLatestATStateWithData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + Integer testHeight = blockchainHeight; + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testGetLatestATStatePostTrimming() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + int maxHeight = blockchainHeight + 100; // more than latest block height + Integer testHeight = blockchainHeight; + + // Trim AT state data + repository.getATRepository().prepareForAtStateTrimming(); + repository.getATRepository().trimAtStates(2, maxHeight, 1000); + + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + + assertEquals(testHeight, atStateData.getHeight()); + // We should always have the latest AT state data available + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + Integer testHeight = blockchainHeight; + + ATData atData = repository.getATRepository().fromATAddress(atAddress); + + byte[] codeHash = atData.getCodeHash(); + Boolean isFinished = Boolean.FALSE; + Integer dataByteOffset = null; + Long expectedValue = null; + Integer minimumFinalHeight = null; + Integer limit = null; + Integer offset = null; + Boolean reverse = null; + + List atStates = repository.getATRepository().getMatchingFinalATStates( + codeHash, + isFinished, + dataByteOffset, + expectedValue, + minimumFinalHeight, + limit, offset, reverse); + + assertEquals(false, atStates.isEmpty()); + assertEquals(1, atStates.size()); + + ATStateData atStateData = atStates.get(0); + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testGetMatchingFinalATStatesWithDataValue() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + Integer testHeight = blockchainHeight; + + ATData atData = repository.getATRepository().fromATAddress(atAddress); + + byte[] codeHash = atData.getCodeHash(); + Boolean isFinished = Boolean.FALSE; + Integer dataByteOffset = MachineState.HEADER_LENGTH + 0; + Long expectedValue = 0L; + Integer minimumFinalHeight = null; + Integer limit = null; + Integer offset = null; + Boolean reverse = null; + + List atStates = repository.getATRepository().getMatchingFinalATStates( + codeHash, + isFinished, + dataByteOffset, + expectedValue, + minimumFinalHeight, + limit, offset, reverse); + + assertEquals(false, atStates.isEmpty()); + assertEquals(1, atStates.size()); + + ATStateData atStateData = atStates.get(0); + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testGetBlockATStatesAtHeightWithData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + doDeploy(repository, deployer, creationBytes, fundingAmount); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + Integer testHeight = 8; + List atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight); + + assertEquals(false, atStates.isEmpty()); + assertEquals(1, atStates.size()); + + ATStateData atStateData = atStates.get(0); + assertEquals(testHeight, atStateData.getHeight()); + // getBlockATStatesAtHeight never returns actual AT state data anyway + assertNull(atStateData.getStateData()); + } + } + + @Test + public void testGetBlockATStatesAtHeightWithoutData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + doDeploy(repository, deployer, creationBytes, fundingAmount); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + + int maxHeight = 8; + Integer testHeight = maxHeight - 2; + + // Trim AT state data + repository.getATRepository().prepareForAtStateTrimming(); + repository.getATRepository().trimAtStates(2, maxHeight, 1000); + + List atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight); + + assertEquals(false, atStates.isEmpty()); + assertEquals(1, atStates.size()); + + ATStateData atStateData = atStates.get(0); + assertEquals(testHeight, atStateData.getHeight()); + // getBlockATStatesAtHeight never returns actual AT state data anyway + assertNull(atStateData.getStateData()); + } + } + + @Test + public void testSaveATStateWithData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + Integer testHeight = blockchainHeight - 2; + ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + + repository.getATRepository().save(atStateData); + + atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + } + } + + @Test + public void testSaveATStateWithoutData() throws DataException { + byte[] creationBytes = buildSimpleAT(); + + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Mint a few blocks + for (int i = 0; i < 10; ++i) + BlockUtils.mintBlock(repository); + int blockchainHeight = repository.getBlockRepository().getBlockchainHeight(); + + Integer testHeight = blockchainHeight - 2; + ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNotNull(atStateData.getStateData()); + + // Clear data + ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(), + atStateData.getHeight(), + /*StateData*/ null, + atStateData.getStateHash(), + atStateData.getFees(), + atStateData.isInitial()); + repository.getATRepository().save(newAtStateData); + + atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight); + + assertEquals(testHeight, atStateData.getHeight()); + assertNull(atStateData.getStateData()); + } + } + + private byte[] buildSimpleAT() { + // Pretend we use 4 values in data segment + int addrCounter = 4; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + try { + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_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; + } + +}