();
}
@@ -288,8 +308,23 @@ public class Block {
return new BigInteger(1, hash);
}
- private long calcNextBlockTimestamp(Account nextBlockGenerator) throws DataException {
- BigInteger hashValue = calcBlockHash();
+ private BigInteger calcNextBlockHash(int nextBlockVersion, byte[] preVersion3GeneratorSignature, PublicKeyAccount nextBlockGenerator) {
+ byte[] hashData;
+
+ if (nextBlockVersion < 3)
+ hashData = preVersion3GeneratorSignature;
+ else
+ hashData = Bytes.concat(this.blockData.getSignature(), nextBlockGenerator.getPublicKey());
+
+ // Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks)
+ byte[] hash = Crypto.digest(hashData);
+
+ // Convert hash to BigInteger form
+ return new BigInteger(1, hash);
+ }
+
+ private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException {
+ BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator);
BigInteger target = calcGeneratorsTarget(nextBlockGenerator);
// If target is zero then generator has no balance so return longest value
@@ -417,6 +452,12 @@ public class Block {
* Recalculate block's generator signature.
*
* Requires block's {@code generator} being a {@code PrivateKeyAccount}.
+ *
+ * Generator signature is made by the generator signing the following data:
+ *
+ * previous block's generator signature + this block's generating balance + generator's public key
+ *
+ * (Previous block's generator signature is extracted from this block's reference).
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
@@ -538,11 +579,12 @@ public class Block {
return ValidationResult.GENERATOR_NOT_ACCEPTED;
// XXX Odd gen1 test: "CHECK IF FIRST BLOCK OF USER"
- // Is the comment wrong and this each second elapsed allows generator to test a new "target" window against hashValue?
+ // Is the comment wrong? Does each second elapsed allows generator to test a new "target" window against hashValue?
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
- // Check CIYAM AT
+ // Process CIYAM ATs, prepending AT-Transactions to block then compare post-execution checksums
+ // XXX We should pre-calculate, and cache, next block's AT-transactions after processing each block to save repeated work
if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) {
// TODO
// try {
@@ -591,8 +633,8 @@ public class Block {
this.repository.discardChanges();
} catch (DataException e) {
/*
- * Discard failure most likely due to prior DataException, so catch discardChanges' DataException and discard. Prior DataException propagates to
- * caller. Successful completion of try-block continues on after discard.
+ * discardChanges failure most likely due to prior DataException, so catch discardChanges' DataException and ignore. Prior DataException
+ * propagates to caller.
*/
}
}
diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java
index 3a2b4074..a35ca067 100644
--- a/src/qora/block/BlockChain.java
+++ b/src/qora/block/BlockChain.java
@@ -43,6 +43,7 @@ public class BlockChain {
private static final long ISSUE_ASSET_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ISSUE ASSET transactions
private static final long CREATE_ORDER_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE ORDER transactions
private static final long ARBITRARY_TRANSACTION_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ARBITRARY transactions
+ private static final long DEPLOY_AT_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 DEPLOY AT transactions
/**
* Some sort start-up/initialization/checking method.
@@ -173,4 +174,11 @@ public class BlockChain {
return ARBITRARY_TRANSACTION_V2_TIMESTAMP;
}
+ public static long getDeployATV2Timestamp() {
+ if (Settings.getInstance().isTestNet())
+ return 0;
+
+ return DEPLOY_AT_V2_TIMESTAMP;
+ }
+
}
diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java
new file mode 100644
index 00000000..45d77600
--- /dev/null
+++ b/src/qora/transaction/DeployATTransaction.java
@@ -0,0 +1,217 @@
+package qora.transaction;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.base.Utf8;
+
+import data.transaction.DeployATTransactionData;
+import data.transaction.TransactionData;
+import qora.account.Account;
+import qora.assets.Asset;
+import qora.at.AT;
+import qora.block.BlockChain;
+import qora.crypto.Crypto;
+import repository.DataException;
+import repository.Repository;
+import transform.Transformer;
+
+public class DeployATTransaction extends Transaction {
+
+ // Properties
+ private DeployATTransactionData deployATTransactionData;
+
+ // Other useful constants
+ public static final int MAX_NAME_SIZE = 200;
+ public static final int MAX_DESCRIPTION_SIZE = 2000;
+ public static final int MAX_AT_TYPE_SIZE = 200;
+ public static final int MAX_TAGS_SIZE = 200;
+ public static final int MAX_CREATION_BYTES_SIZE = 100_000;
+
+ // Constructors
+
+ public DeployATTransaction(Repository repository, TransactionData transactionData) {
+ super(repository, transactionData);
+
+ this.deployATTransactionData = (DeployATTransactionData) this.transactionData;
+ }
+
+ // More information
+
+ @Override
+ public List getRecipientAccounts() throws DataException {
+ return new ArrayList();
+ }
+
+ @Override
+ public boolean isInvolved(Account account) throws DataException {
+ String address = account.getAddress();
+
+ if (address.equals(this.getCreator().getAddress()))
+ return true;
+
+ if (address.equals(this.getATAccount().getAddress()))
+ return true;
+
+ return false;
+ }
+
+ @Override
+ public BigDecimal getAmount(Account account) throws DataException {
+ String address = account.getAddress();
+ BigDecimal amount = BigDecimal.ZERO.setScale(8);
+
+ if (address.equals(this.getCreator().getAddress()))
+ amount = amount.subtract(this.deployATTransactionData.getAmount()).subtract(this.transactionData.getFee());
+
+ if (address.equals(this.getATAccount().getAddress()))
+ amount = amount.add(this.deployATTransactionData.getAmount());
+
+ return amount;
+ }
+
+ /** Make sure deployATTransactionData has an ATAddress */
+ private void ensureATAddress() throws DataException {
+ if (this.deployATTransactionData.getATAddress() != null)
+ return;
+
+ int blockHeight = this.getHeight();
+ if (blockHeight == 0)
+ blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
+
+ try {
+ byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8");
+ byte[] description = this.deployATTransactionData.getDescription().replaceAll("\\s", "").getBytes("UTF-8");
+ byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
+ byte[] creationBytes = this.deployATTransactionData.getCreationBytes();
+
+ ByteBuffer byteBuffer = ByteBuffer
+ .allocate(name.length + description.length + creatorPublicKey.length + creationBytes.length + Transformer.INT_LENGTH);
+
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ byteBuffer.put(name);
+ byteBuffer.put(description);
+ byteBuffer.put(creatorPublicKey);
+ byteBuffer.put(creationBytes);
+ byteBuffer.putInt(blockHeight);
+
+ String atAddress = Crypto.toATAddress(byteBuffer.array());
+
+ this.deployATTransactionData.setATAddress(atAddress);
+ } catch (UnsupportedEncodingException e) {
+ throw new DataException("Unable to generate AT account from Deploy AT transaction data", e);
+ }
+ }
+
+ // Navigation
+
+ public Account getATAccount() throws DataException {
+ ensureATAddress();
+
+ return new Account(this.repository, this.deployATTransactionData.getATAddress());
+ }
+
+ // Processing
+
+ @Override
+ public ValidationResult isValid() throws DataException {
+ if (this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getATReleaseHeight())
+ return ValidationResult.NOT_YET_RELEASED;
+
+ // Check name size bounds
+ int nameLength = Utf8.encodedLength(deployATTransactionData.getName());
+ if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
+ return ValidationResult.INVALID_NAME_LENGTH;
+
+ // Check description size bounds
+ int descriptionlength = Utf8.encodedLength(deployATTransactionData.getDescription());
+ if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
+ return ValidationResult.INVALID_DESCRIPTION_LENGTH;
+
+ // Check AT-type size bounds
+ int ATTypeLength = Utf8.encodedLength(deployATTransactionData.getATType());
+ if (ATTypeLength < 1 || ATTypeLength > MAX_AT_TYPE_SIZE)
+ return ValidationResult.INVALID_AT_TYPE_LENGTH;
+
+ // Check tags size bounds
+ int tagsLength = Utf8.encodedLength(deployATTransactionData.getTags());
+ if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
+ return ValidationResult.INVALID_TAGS_LENGTH;
+
+ // Check amount is positive
+ if (deployATTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
+
+ // Check fee is positive
+ if (deployATTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
+ return ValidationResult.NEGATIVE_FEE;
+
+ // Check reference is correct
+ Account creator = getCreator();
+
+ if (!Arrays.equals(creator.getLastReference(), deployATTransactionData.getReference()))
+ return ValidationResult.INVALID_REFERENCE;
+
+ // Check creator has enough funds
+ BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount());
+ if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
+ return ValidationResult.NO_BALANCE;
+
+ // Check creation bytes are valid (for v3+)
+ byte[] creationBytes = deployATTransactionData.getCreationBytes();
+ short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
+
+ if (version >= 3) {
+ // Do actual validation
+ } else {
+ // Skip validation for old, dead ATs
+ }
+
+ return ValidationResult.OK;
+ }
+
+ @Override
+ public void process() throws DataException {
+ ensureATAddress();
+
+ // Deploy AT, saving into repository
+ AT at = new AT(this.repository, this.deployATTransactionData);
+ at.deploy();
+
+ // Save this transaction itself
+ this.repository.getTransactionRepository().save(this.transactionData);
+
+ // Update creator's balance
+ Account creator = getCreator();
+ creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getAmount()));
+ creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getFee()));
+
+ // Update creator's reference
+ creator.setLastReference(deployATTransactionData.getSignature());
+ }
+
+ @Override
+ public void orphan() throws DataException {
+ // Delete AT from repository
+ AT at = new AT(this.repository, this.deployATTransactionData);
+ at.undeploy();
+
+ // Delete this transaction itself
+ this.repository.getTransactionRepository().delete(deployATTransactionData);
+
+ // Update creator's balance
+ Account creator = getCreator();
+ creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getAmount()));
+ creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getFee()));
+
+ // Update creator's reference
+ creator.setLastReference(deployATTransactionData.getReference());
+ }
+
+}
diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java
index ed2330b8..d3ae1bc1 100644
--- a/src/qora/transaction/GenesisTransaction.java
+++ b/src/qora/transaction/GenesisTransaction.java
@@ -118,7 +118,7 @@ public class GenesisTransaction extends Transaction {
@Override
public ValidationResult isValid() {
// Check amount is zero or positive
- if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) >= 0)
+ if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java
index 42b7c418..aaba8836 100644
--- a/src/qora/transaction/Transaction.java
+++ b/src/qora/transaction/Transaction.java
@@ -24,8 +24,27 @@ public abstract class Transaction {
// Transaction types
public enum TransactionType {
- GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY(
- 10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17);
+ GENESIS(1),
+ PAYMENT(2),
+ REGISTER_NAME(3),
+ UPDATE_NAME(4),
+ SELL_NAME(5),
+ CANCEL_SELL_NAME(6),
+ BUY_NAME(7),
+ CREATE_POLL(8),
+ VOTE_ON_POLL(9),
+ ARBITRARY(10),
+ ISSUE_ASSET(11),
+ TRANSFER_ASSET(12),
+ CREATE_ASSET_ORDER(13),
+ CANCEL_ASSET_ORDER(14),
+ MULTIPAYMENT(15),
+ DEPLOY_AT(16),
+ MESSAGE(17),
+ DELEGATION(18),
+ SUPERNODE(19),
+ AIRDROP(20),
+ AT(21);
public final int value;
@@ -42,14 +61,44 @@ public abstract class Transaction {
// Validation results
public enum ValidationResult {
- OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_VALUE_LENGTH(
- 8), NAME_ALREADY_REGISTERED(9), NAME_DOES_NOT_EXIST(10), INVALID_NAME_OWNER(11), NAME_ALREADY_FOR_SALE(12), NAME_NOT_FOR_SALE(
- 13), BUYER_ALREADY_OWNER(14), INVALID_AMOUNT(15), INVALID_SELLER(16), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH(
- 18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(21), POLL_ALREADY_EXISTS(22), POLL_DOES_NOT_EXIST(
- 24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION(26), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(
- 28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(31), ORDER_DOES_NOT_EXIST(
- 32), INVALID_ORDER_CREATOR(33), INVALID_PAYMENTS_COUNT(
- 34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000);
+ OK(1),
+ INVALID_ADDRESS(2),
+ NEGATIVE_AMOUNT(3),
+ NEGATIVE_FEE(4),
+ NO_BALANCE(5),
+ INVALID_REFERENCE(6),
+ INVALID_NAME_LENGTH(7),
+ INVALID_VALUE_LENGTH(8),
+ NAME_ALREADY_REGISTERED(9),
+ NAME_DOES_NOT_EXIST(10),
+ INVALID_NAME_OWNER(11),
+ NAME_ALREADY_FOR_SALE(12),
+ NAME_NOT_FOR_SALE(13),
+ BUYER_ALREADY_OWNER(14),
+ INVALID_AMOUNT(15),
+ INVALID_SELLER(16),
+ NAME_NOT_LOWER_CASE(17),
+ INVALID_DESCRIPTION_LENGTH(18),
+ INVALID_OPTIONS_COUNT(19),
+ INVALID_OPTION_LENGTH(20),
+ DUPLICATE_OPTION(21),
+ POLL_ALREADY_EXISTS(22),
+ POLL_DOES_NOT_EXIST(24),
+ POLL_OPTION_DOES_NOT_EXIST(25),
+ ALREADY_VOTED_FOR_THAT_OPTION(26),
+ INVALID_DATA_LENGTH(27),
+ INVALID_QUANTITY(28),
+ ASSET_DOES_NOT_EXIST(29),
+ INVALID_RETURN(30),
+ HAVE_EQUALS_WANT(31),
+ ORDER_DOES_NOT_EXIST(32),
+ INVALID_ORDER_CREATOR(33),
+ INVALID_PAYMENTS_COUNT(34),
+ NEGATIVE_PRICE(35),
+ INVALID_TAGS_LENGTH(37),
+ INVALID_AT_TYPE_LENGTH(38),
+ ASSET_ALREADY_EXISTS(43),
+ NOT_YET_RELEASED(1000);
public final int value;
@@ -147,6 +196,9 @@ public abstract class Transaction {
case MESSAGE:
return new MessageTransaction(repository, transactionData);
+ case DEPLOY_AT:
+ return new DeployATTransaction(repository, transactionData);
+
default:
throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository");
}
diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java
index 40bda07b..5325c9f0 100644
--- a/src/qora/transaction/TransferAssetTransaction.java
+++ b/src/qora/transaction/TransferAssetTransaction.java
@@ -84,9 +84,9 @@ public class TransferAssetTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
- // Are IssueAssetTransactions even allowed at this point?
+ // Are TransferAssetTransactions even allowed at this point?
// XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
- if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp())
+ if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp())
return ValidationResult.NOT_YET_RELEASED;
// Check reference is correct
diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java
new file mode 100644
index 00000000..fef0c76e
--- /dev/null
+++ b/src/repository/ATRepository.java
@@ -0,0 +1,24 @@
+package repository;
+
+import data.at.ATData;
+import data.at.ATStateData;
+
+public interface ATRepository {
+
+ // CIYAM AutomatedTransactions
+
+ public ATData fromATAddress(String atAddress) throws DataException;
+
+ public void save(ATData atData) throws DataException;
+
+ public void delete(String atAddress) throws DataException;
+
+ // AT States
+
+ public ATStateData getATState(String atAddress, int height) throws DataException;
+
+ public void save(ATStateData atStateData) throws DataException;
+
+ public void delete(String atAddress, int height) throws DataException;
+
+}
diff --git a/src/repository/Repository.java b/src/repository/Repository.java
index fa969947..f5bc16ba 100644
--- a/src/repository/Repository.java
+++ b/src/repository/Repository.java
@@ -2,6 +2,8 @@ package repository;
public interface Repository extends AutoCloseable {
+ public ATRepository getATRepository();
+
public AccountRepository getAccountRepository();
public AssetRepository getAssetRepository();
diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java
new file mode 100644
index 00000000..5c2cc88d
--- /dev/null
+++ b/src/repository/hsqldb/HSQLDBATRepository.java
@@ -0,0 +1,117 @@
+package repository.hsqldb;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import data.at.ATData;
+import data.at.ATStateData;
+import repository.ATRepository;
+import repository.DataException;
+
+public class HSQLDBATRepository implements ATRepository {
+
+ protected HSQLDBRepository repository;
+
+ public HSQLDBATRepository(HSQLDBRepository repository) {
+ this.repository = repository;
+ }
+
+ // ATs
+
+ @Override
+ public ATData fromATAddress(String atAddress) throws DataException {
+ try (ResultSet resultSet = this.repository
+ .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE AT_address = ?", atAddress)) {
+ if (resultSet == null)
+ return null;
+
+ int version = resultSet.getInt(1);
+ byte[] codeBytes = resultSet.getBytes(2); // Actually BLOB
+ boolean isSleeping = resultSet.getBoolean(3);
+
+ Integer sleepUntilHeight = resultSet.getInt(4);
+ if (resultSet.wasNull())
+ sleepUntilHeight = null;
+
+ boolean isFinished = resultSet.getBoolean(5);
+ boolean hadFatalError = resultSet.getBoolean(6);
+ boolean isFrozen = resultSet.getBoolean(7);
+
+ BigDecimal frozenBalance = resultSet.getBigDecimal(8);
+ if (resultSet.wasNull())
+ frozenBalance = null;
+
+ byte[] deploySignature = resultSet.getBytes(9);
+
+ return new ATData(atAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, deploySignature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch AT from repository", e);
+ }
+ }
+
+ @Override
+ public void save(ATData atData) throws DataException {
+ HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
+
+ saveHelper.bind("AT_address", atData.getATAddress()).bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes())
+ .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("deploy_signature", atData.getDeploySignature());
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to save AT into repository", e);
+ }
+ }
+
+ @Override
+ public void delete(String atAddress) throws DataException {
+ try {
+ this.repository.delete("ATs", "atAddress = ?", atAddress);
+ // AT States also deleted via ON DELETE CASCADE
+ } catch (SQLException e) {
+ throw new DataException("Unable to delete AT from repository", e);
+ }
+ }
+
+ // AT State
+
+ @Override
+ public ATStateData getATState(String atAddress, int height) throws DataException {
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
+ if (resultSet == null)
+ return null;
+
+ byte[] stateData = resultSet.getBytes(1); // Actually BLOB
+
+ return new ATStateData(atAddress, height, stateData);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch AT State from repository", e);
+ }
+ }
+
+ @Override
+ public void save(ATStateData atStateData) throws DataException {
+ HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
+
+ saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()).bind("state_data", atStateData.getStateData());
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to save AT State into repository", e);
+ }
+ }
+
+ @Override
+ public void delete(String atAddress, int height) throws DataException {
+ try {
+ this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
+ } catch (SQLException e) {
+ throw new DataException("Unable to delete AT State from repository", e);
+ }
+ }
+
+}
diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
index 836b2a1e..f3f1dba7 100644
--- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -77,7 +77,8 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD");
stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD");
stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD");
- stmt.execute("SET FILES SPACE TRUE");
+ stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types
+ stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB
stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )");
stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )");
stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)");
@@ -96,6 +97,9 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)");
stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD");
+ stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
+ stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
+ stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)");
break;
case 1:
@@ -269,7 +273,7 @@ public class HSQLDBDatabaseUpdates {
// Deploy CIYAM AT Transactions
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QoraPublicKey NOT NULL, AT_name ATName NOT NULL, "
+ "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, "
- + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, "
+ + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
@@ -346,6 +350,22 @@ public class HSQLDBDatabaseUpdates {
+ "PRIMARY KEY (name))");
break;
+ case 27:
+ // CIYAM Automated Transactions
+ stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER NOT NULL, code_bytes ATCode NOT NULL, "
+ + "is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, "
+ + "is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))");
+ // For finding executable ATs
+ stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)");
+ // AT state on a per-block basis
+ stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, "
+ + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
+ // Generated AT Transactions
+ stmt.execute(
+ "CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount NOT NULL, message ATMessage, "
+ + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
+ break;
+
default:
// nothing to do
return false;
diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java
index 3081ee55..fc181eea 100644
--- a/src/repository/hsqldb/HSQLDBRepository.java
+++ b/src/repository/hsqldb/HSQLDBRepository.java
@@ -11,6 +11,7 @@ import java.util.TimeZone;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import repository.ATRepository;
import repository.AccountRepository;
import repository.AssetRepository;
import repository.BlockRepository;
@@ -34,6 +35,11 @@ public class HSQLDBRepository implements Repository {
this.connection = connection;
}
+ @Override
+ public ATRepository getATRepository() {
+ return new HSQLDBATRepository(this);
+ }
+
@Override
public AccountRepository getAccountRepository() {
return new HSQLDBAccountRepository(this);
@@ -84,6 +90,12 @@ public class HSQLDBRepository implements Repository {
@Override
public void close() throws DataException {
+ // Already closed? No need to do anything but maybe report double-call
+ if (this.connection == null) {
+ LOGGER.warn("HSQLDBRepository.close() called when repository already closed", new Exception("Repository already closed"));
+ return;
+ }
+
try (Statement stmt = this.connection.createStatement()) {
// Diagnostic check for uncommitted changes
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions")) // TRANSACTION_SIZE() broken?
@@ -96,7 +108,7 @@ public class HSQLDBRepository implements Repository {
boolean inTransaction = resultSet.getBoolean(1);
int transactionCount = resultSet.getInt(2);
if (inTransaction && transactionCount != 0)
- LOGGER.warn("Uncommitted changes (" + transactionCount + ") during repository close");
+ LOGGER.warn("Uncommitted changes (" + transactionCount + ") during repository close", new Exception("Uncommitted repository changes"));
}
// give connection back to the pool
diff --git a/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java
new file mode 100644
index 00000000..a4d54d41
--- /dev/null
+++ b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java
@@ -0,0 +1,63 @@
+package repository.hsqldb.transaction;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import data.transaction.DeployATTransactionData;
+import data.transaction.TransactionData;
+import repository.DataException;
+import repository.hsqldb.HSQLDBRepository;
+import repository.hsqldb.HSQLDBSaver;
+
+public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionRepository {
+
+ public HSQLDBDeployATTransactionRepository(HSQLDBRepository repository) {
+ this.repository = repository;
+ }
+
+ TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
+ try (ResultSet resultSet = this.repository.checkedExecute(
+ "SELECT AT_name, description, AT_type, AT_tags, creation_bytes, amount, AT_address FROM DeployATTransactions WHERE signature = ?", signature)) {
+ if (resultSet == null)
+ return null;
+
+ String name = resultSet.getString(1);
+ String description = resultSet.getString(2);
+ String ATType = resultSet.getString(3);
+ String tags = resultSet.getString(4);
+ byte[] creationBytes = resultSet.getBytes(5);
+ BigDecimal amount = resultSet.getBigDecimal(6).setScale(8);
+
+ // Special null-checking for AT address
+ String ATAddress = resultSet.getString(7);
+ if (resultSet.wasNull())
+ ATAddress = null;
+
+ return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference,
+ signature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch deploy AT transaction from repository", e);
+ }
+ }
+
+ @Override
+ public void save(TransactionData transactionData) throws DataException {
+ DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
+
+ HSQLDBSaver saveHelper = new HSQLDBSaver("DeployATTransactions");
+
+ saveHelper.bind("signature", deployATTransactionData.getSignature()).bind("creator", deployATTransactionData.getCreatorPublicKey())
+ .bind("AT_name", deployATTransactionData.getName()).bind("description", deployATTransactionData.getDescription())
+ .bind("AT_type", deployATTransactionData.getATType()).bind("AT_tags", deployATTransactionData.getTags())
+ .bind("creation_bytes", deployATTransactionData.getCreationBytes()).bind("amount", deployATTransactionData.getAmount())
+ .bind("AT_address", deployATTransactionData.getATAddress());
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to save deploy AT transaction into repository", e);
+ }
+ }
+
+}
diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
index f6d7fc59..316ee6a7 100644
--- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -35,6 +35,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository;
private HSQLDBCancelOrderTransactionRepository cancelOrderTransactionRepository;
private HSQLDBMultiPaymentTransactionRepository multiPaymentTransactionRepository;
+ private HSQLDBDeployATTransactionRepository deployATTransactionRepository;
private HSQLDBMessageTransactionRepository messageTransactionRepository;
public HSQLDBTransactionRepository(HSQLDBRepository repository) {
@@ -54,6 +55,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository);
this.cancelOrderTransactionRepository = new HSQLDBCancelOrderTransactionRepository(repository);
this.multiPaymentTransactionRepository = new HSQLDBMultiPaymentTransactionRepository(repository);
+ this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository);
this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository);
}
@@ -146,6 +148,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
case MULTIPAYMENT:
return this.multiPaymentTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
+ case DEPLOY_AT:
+ return this.deployATTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
+
case MESSAGE:
return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
@@ -304,6 +309,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.multiPaymentTransactionRepository.save(transactionData);
break;
+ case DEPLOY_AT:
+ this.deployATTransactionRepository.save(transactionData);
+ break;
+
case MESSAGE:
this.messageTransactionRepository.save(transactionData);
break;
diff --git a/src/test/ATTests.java b/src/test/ATTests.java
new file mode 100644
index 00000000..076bb927
--- /dev/null
+++ b/src/test/ATTests.java
@@ -0,0 +1,96 @@
+package test;
+
+import static org.junit.Assert.*;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import com.google.common.hash.HashCode;
+
+import data.block.BlockData;
+import data.block.BlockTransactionData;
+import data.transaction.DeployATTransactionData;
+import qora.transaction.DeployATTransaction;
+import repository.DataException;
+import repository.Repository;
+import repository.RepositoryManager;
+import transform.TransformationException;
+import utils.Base58;
+
+public class ATTests extends Common {
+
+ @Test
+ public void testATAccount() throws TransformationException, DataException {
+ // 2dZ4megUyNoYYY7qWmuSd4xw1yUKgPPF97yBbeddh8aKuC8PLpz7Xvf3r6Zjv1zwGrR8fEAHuaztCPD4KQp76KdL at height 125598
+ // AT address: AaaUn82XV4YcUtsQ3rHa5ZgqyiK35rVfE3
+
+ String expectedAddress = "AaaUn82XV4YcUtsQ3rHa5ZgqyiK35rVfE3";
+
+ byte[] creatorPublicKey = HashCode.fromString("c74d71ecec6b37890f26573186e634986cc90a507af01749f92aa2c7c95ad05f").asBytes();
+ String name = "QORABURST @ 1.00";
+ String description = "Initiators BURST address: BURST-LKGW-Z2JK-EZ99-E7CUE";
+ String ATType = "acct";
+ String tags = "acct,atomic cross chain tx,initiate,initiator";
+ byte[] creationBytes = HashCode
+ .fromString("010000000100010000000000" + "0094357700" + "000000bf"
+ + "3501030900000006040000000900000029302009000000040000000f1ab4000000330403090000003525010a000000260a000000320903350703090000003526010a0000001b0a000000cd322801331601000000003317010100000033180102000000331901030000003505020a0000001b0a000000a1320b033205041e050000001833000509000000320a033203041ab400000033160105000000331701060000003318010700000033190108000000320304320b033203041ab7"
+ + "00000048"
+ + "5e211280259d2f3130248482c2dfc53be2fd5f9bedc9bc21425f951e8097a21900000000c80000003ac8716ad810191acf270d22e9f47f27806256c10d6ba6144900000000000000")
+ .asBytes();
+ BigDecimal amount = BigDecimal.valueOf(500.0).setScale(8);
+ BigDecimal fee = BigDecimal.valueOf(20.0).setScale(8);
+ long timestamp = 1439997077932L;
+ byte[] reference = Base58.decode("2D3jX1pEgu6irsQ7QzJb85QP1D9M45dNyP5M9a3WFHndU5ZywF4F5pnUurcbzMnGMcTwpAY6H7DuLw8cUBU66ao1");
+ byte[] signature = Base58.decode("2dZ4megUyNoYYY7qWmuSd4xw1yUKgPPF97yBbeddh8aKuC8PLpz7Xvf3r6Zjv1zwGrR8fEAHuaztCPD4KQp76KdL");
+
+ DeployATTransactionData transactionData = new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee,
+ timestamp, reference, signature);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ repository.getTransactionRepository().save(transactionData);
+
+ DeployATTransaction transaction = new DeployATTransaction(repository, transactionData);
+
+ // Fake entry for this transaction at block height 125598 if it doesn't already exist
+ if (transaction.getHeight() == 0) {
+ byte[] blockSignature = Base58.decode(
+ "2amu634LnAbxeLfDtWdTLiCWtKu1XM2XLK9o6fDM7yGNNoh5Tq2KxSLdx8AS486zUU1wYNGCm8mcGxjMiww979MxdPVB2PQzaKrW2aFn9hpdSNN6Nk7EmeYKwsZdx9tkpHfBt5thSrUUrhzXJju9KYCAP6p3Ty4zccFkaxCP15j332U");
+ byte[] generatorSignature = Arrays.copyOfRange(blockSignature, 0, 64);
+ byte[] transactionsSignature = Arrays.copyOfRange(blockSignature, 64, 128);
+
+ // Check block exists too
+ if (repository.getBlockRepository().fromSignature(blockSignature) == null) {
+ int version = 2;
+ byte[] blockReference = blockSignature;
+ int transactionCount = 0;
+ BigDecimal totalFees = BigDecimal.valueOf(70.0).setScale(8);
+ int height = 125598;
+ long blockTimestamp = 1439997158336L;
+ BigDecimal generatingBalance = BigDecimal.valueOf(1440368826L).setScale(8);
+ byte[] generatorPublicKey = Base58.decode("X4s833bbtghh7gejmaBMbWqD44HrUobw93ANUuaNhFc");
+ byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes();
+ BigDecimal atFees = BigDecimal.valueOf(50.0).setScale(8);
+
+ BlockData blockData = new BlockData(version, blockReference, transactionCount, totalFees, transactionsSignature, height, blockTimestamp,
+ generatingBalance, generatorPublicKey, generatorSignature, atBytes, atFees);
+
+ repository.getBlockRepository().save(blockData);
+ }
+
+ int sequence = 0;
+
+ BlockTransactionData blockTransactionData = new BlockTransactionData(blockSignature, sequence, signature);
+ repository.getBlockRepository().save(blockTransactionData);
+ }
+
+ String actualAddress = transaction.getATAccount().getAddress();
+
+ repository.discardChanges();
+
+ assertEquals(expectedAddress, actualAddress);
+ }
+ }
+
+}
diff --git a/src/test/RepositoryTests.java b/src/test/RepositoryTests.java
index e2df05ec..a37b71c6 100644
--- a/src/test/RepositoryTests.java
+++ b/src/test/RepositoryTests.java
@@ -2,6 +2,8 @@ package test;
import static org.junit.Assert.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.junit.Test;
import repository.DataException;
@@ -10,6 +12,8 @@ import repository.RepositoryManager;
public class RepositoryTests extends Common {
+ private static final Logger LOGGER = LogManager.getLogger(RepositoryTests.class);
+
@Test
public void testGetRepository() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -45,6 +49,8 @@ public class RepositoryTests extends Common {
fail();
} catch (NullPointerException | DataException e) {
}
+
+ LOGGER.warn("Expect \"repository already closed\" complaint below");
}
}
diff --git a/src/test/SaveTests.java b/src/test/SaveTests.java
index e57ab170..1338e3b8 100644
--- a/src/test/SaveTests.java
+++ b/src/test/SaveTests.java
@@ -27,6 +27,8 @@ public class SaveTests extends Common {
BigDecimal.ONE, Instant.now().getEpochSecond(), reference, signature);
repository.getTransactionRepository().save(paymentTransactionData);
+
+ repository.discardChanges();
}
}
diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java
index 9365f91f..f976c59f 100644
--- a/src/test/TransactionTests.java
+++ b/src/test/TransactionTests.java
@@ -4,6 +4,7 @@ import static org.junit.Assert.*;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -616,7 +617,7 @@ public class TransactionTests {
String assetName = "test asset";
String description = "test asset description";
long quantity = 1_000_000L;
- boolean isDivisible = false;
+ boolean isDivisible = true;
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
@@ -956,16 +957,20 @@ public class TransactionTests {
assertNotNull(originalOrderData);
assertFalse(originalOrderData.getIsClosed());
+ // Unfulfilled order: "buyer" has 10 QORA and wants to buy "test asset" at a price of 50 "test asset" per QORA.
+ // buyer's order: have=QORA, amount=10, want=test-asset, price=50 (test-asset per QORA, so max return is 500 test-asset)
+
// Original asset owner (sender) will sell asset to "buyer"
// Order: seller has 40 "test asset" and wants to buy QORA at a price of 1/60 QORA per "test asset".
// This order should be a partial match for original order, and at a better price than asked
- long haveAssetId = Asset.QORA;
+ long haveAssetId = assetId;
BigDecimal amount = BigDecimal.valueOf(40).setScale(8);
- long wantAssetId = assetId;
- BigDecimal price = BigDecimal.ONE.setScale(8).divide(BigDecimal.valueOf(60).setScale(8));
+ long wantAssetId = Asset.QORA;
+ BigDecimal price = BigDecimal.ONE.setScale(8).divide(BigDecimal.valueOf(60).setScale(8), RoundingMode.DOWN);
BigDecimal fee = BigDecimal.ONE;
long timestamp = parentBlockData.getTimestamp() + 1_000;
+ BigDecimal senderPreTradeWantBalance = sender.getConfirmedBalance(wantAssetId);
CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(sender.getPublicKey(), haveAssetId, wantAssetId, amount, price,
fee, timestamp, reference);
@@ -989,20 +994,19 @@ public class TransactionTests {
byte[] orderId = createOrderTransactionData.getSignature();
OrderData orderData = assetRepo.fromOrderId(orderId);
assertNotNull(orderData);
- assertFalse(orderData.getIsFulfilled());
// Check order has trades
List trades = assetRepo.getOrdersTrades(orderId);
assertNotNull(trades);
- assertEquals(1, trades.size());
+ assertEquals("Trade didn't happen", 1, trades.size());
TradeData tradeData = trades.get(0);
// Check trade has correct values
- BigDecimal expectedAmount = amount.multiply(price);
+ BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8);
BigDecimal actualAmount = tradeData.getAmount();
assertTrue(expectedAmount.compareTo(actualAmount) == 0);
- BigDecimal expectedPrice = originalOrderData.getPrice().multiply(amount);
+ BigDecimal expectedPrice = amount;
BigDecimal actualPrice = tradeData.getPrice();
assertTrue(expectedPrice.compareTo(actualPrice) == 0);
@@ -1017,10 +1021,17 @@ public class TransactionTests {
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
// Check seller's QORA balance
- expectedBalance = initialSenderBalance.subtract(BigDecimal.ONE).subtract(BigDecimal.ONE);
+ expectedBalance = senderPreTradeWantBalance.subtract(BigDecimal.ONE).add(expectedAmount);
actualBalance = sender.getConfirmedBalance(wantAssetId);
assertTrue(expectedBalance.compareTo(actualBalance) == 0);
+ // Check seller's order is correctly fulfilled
+ assertTrue(orderData.getIsFulfilled());
+
+ // Check buyer's order is still not fulfilled
+ OrderData buyersOrderData = assetRepo.fromOrderId(originalOrderData.getOrderId());
+ assertFalse(buyersOrderData.getIsFulfilled());
+
// Orphan transaction
block.orphan();
repository.saveChanges();
diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java
index 0542948a..7d7336ae 100644
--- a/src/transform/block/BlockTransformer.java
+++ b/src/transform/block/BlockTransformer.java
@@ -289,6 +289,24 @@ public class BlockTransformer extends Transformer {
}
}
+ public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator)
+ throws TransformationException {
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
+
+ bytes.write(generatorSignature);
+
+ bytes.write(Longs.toByteArray(generatingBalance.longValue()));
+
+ // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
+ bytes.write(Bytes.ensureCapacity(generator.getPublicKey(), GENERATOR_LENGTH, 0));
+
+ return bytes.toByteArray();
+ } catch (IOException e) {
+ throw new TransformationException(e);
+ }
+ }
+
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(
GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH);
diff --git a/src/transform/transaction/DeployATTransactionTransformer.java b/src/transform/transaction/DeployATTransactionTransformer.java
new file mode 100644
index 00000000..0699084f
--- /dev/null
+++ b/src/transform/transaction/DeployATTransactionTransformer.java
@@ -0,0 +1,188 @@
+package transform.transaction;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+
+import org.json.simple.JSONObject;
+
+import com.google.common.base.Utf8;
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+
+import data.transaction.TransactionData;
+import qora.account.PublicKeyAccount;
+import qora.block.BlockChain;
+import qora.transaction.DeployATTransaction;
+import data.transaction.DeployATTransactionData;
+import transform.TransformationException;
+import utils.Serialization;
+
+public class DeployATTransactionTransformer extends TransactionTransformer {
+
+ // Property lengths
+ private static final int CREATOR_LENGTH = PUBLIC_KEY_LENGTH;
+ private static final int NAME_SIZE_LENGTH = INT_LENGTH;
+ private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH;
+ private static final int AT_TYPE_SIZE_LENGTH = INT_LENGTH;
+ private static final int TAGS_SIZE_LENGTH = INT_LENGTH;
+ private static final int CREATION_BYTES_SIZE_LENGTH = INT_LENGTH;
+ private static final int AMOUNT_LENGTH = LONG_LENGTH;
+
+ private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + AT_TYPE_SIZE_LENGTH
+ + TAGS_SIZE_LENGTH + CREATION_BYTES_SIZE_LENGTH + AMOUNT_LENGTH;
+
+ static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+ long timestamp = byteBuffer.getLong();
+
+ byte[] reference = new byte[REFERENCE_LENGTH];
+ byteBuffer.get(reference);
+
+ byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
+
+ String name = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_NAME_SIZE);
+
+ String description = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_DESCRIPTION_SIZE);
+
+ String ATType = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_AT_TYPE_SIZE);
+
+ String tags = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_TAGS_SIZE);
+
+ int creationBytesSize = byteBuffer.getInt();
+ if (creationBytesSize <= 0 || creationBytesSize > DeployATTransaction.MAX_CREATION_BYTES_SIZE)
+ throw new TransformationException("Creation bytes size invalid in DeployAT transaction");
+
+ byte[] creationBytes = new byte[creationBytesSize];
+ byteBuffer.get(creationBytes);
+
+ BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
+
+ BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
+
+ byte[] signature = new byte[SIGNATURE_LENGTH];
+ byteBuffer.get(signature);
+
+ return new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, signature);
+ }
+
+ public static int getDataLength(TransactionData transactionData) throws TransformationException {
+ DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
+
+ int dataLength = TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(deployATTransactionData.getName())
+ + Utf8.encodedLength(deployATTransactionData.getDescription()) + Utf8.encodedLength(deployATTransactionData.getATType())
+ + Utf8.encodedLength(deployATTransactionData.getTags()) + deployATTransactionData.getCreationBytes().length;
+
+ return dataLength;
+ }
+
+ public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
+ try {
+ DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ bytes.write(Ints.toByteArray(deployATTransactionData.getType().value));
+ bytes.write(Longs.toByteArray(deployATTransactionData.getTimestamp()));
+ bytes.write(deployATTransactionData.getReference());
+
+ bytes.write(deployATTransactionData.getCreatorPublicKey());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getName());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getDescription());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getATType());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
+
+ bytes.write(deployATTransactionData.getCreationBytes());
+
+ Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
+
+ Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
+
+ if (deployATTransactionData.getSignature() != null)
+ bytes.write(deployATTransactionData.getSignature());
+
+ return bytes.toByteArray();
+ } catch (IOException | ClassCastException e) {
+ throw new TransformationException(e);
+ }
+ }
+
+ /**
+ * In Qora v1, the bytes used for verification omit AT-type and tags so we need to test for v1-ness and adjust the bytes
+ * accordingly.
+ *
+ * @param transactionData
+ * @return byte[]
+ * @throws TransformationException
+ */
+ public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
+ if (transactionData.getTimestamp() >= BlockChain.getDeployATV2Timestamp())
+ return TransactionTransformer.toBytesForSigningImpl(transactionData);
+
+ // Special v1 version
+
+ // Easier to start from scratch
+ try {
+ DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ bytes.write(Ints.toByteArray(deployATTransactionData.getType().value));
+ bytes.write(Longs.toByteArray(deployATTransactionData.getTimestamp()));
+ bytes.write(deployATTransactionData.getReference());
+
+ bytes.write(deployATTransactionData.getCreatorPublicKey());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getName());
+
+ Serialization.serializeSizedString(bytes, deployATTransactionData.getDescription());
+
+ // Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getATType());
+
+ // Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
+
+ bytes.write(deployATTransactionData.getCreationBytes());
+
+ Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
+
+ Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
+
+ if (deployATTransactionData.getSignature() != null)
+ bytes.write(deployATTransactionData.getSignature());
+
+ return bytes.toByteArray();
+ } catch (IOException | ClassCastException e) {
+ throw new TransformationException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
+ JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
+
+ try {
+ DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData;
+
+ byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey();
+
+ json.put("creator", PublicKeyAccount.getAddress(creatorPublicKey));
+ json.put("creatorPublicKey", HashCode.fromBytes(creatorPublicKey).toString());
+ json.put("name", deployATTransactionData.getName());
+ json.put("description", deployATTransactionData.getDescription());
+ json.put("atType", deployATTransactionData.getATType());
+ json.put("tags", deployATTransactionData.getTags());
+ json.put("creationBytes", HashCode.fromBytes(deployATTransactionData.getCreationBytes()).toString());
+ json.put("amount", deployATTransactionData.getAmount().toPlainString());
+ } catch (ClassCastException e) {
+ throw new TransformationException(e);
+ }
+
+ return json;
+ }
+
+}
diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java
index 15b873e6..a4ec1b5d 100644
--- a/src/transform/transaction/IssueAssetTransactionTransformer.java
+++ b/src/transform/transaction/IssueAssetTransactionTransformer.java
@@ -99,9 +99,14 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity()));
bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0));
- // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference.
- if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp())
- bytes.write(issueAssetTransactionData.getSignature());
+ // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes Asset's reference which is the IssueAssetTransaction's signature
+ if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) {
+ byte[] assetReference = issueAssetTransactionData.getSignature();
+ if (assetReference != null)
+ bytes.write(assetReference);
+ else
+ bytes.write(new byte[ASSET_REFERENCE_LENGTH]);
+ }
Serialization.serializeBigDecimal(bytes, issueAssetTransactionData.getFee());
diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java
index 3732f1ee..232ce38f 100644
--- a/src/transform/transaction/TransactionTransformer.java
+++ b/src/transform/transaction/TransactionTransformer.java
@@ -92,6 +92,9 @@ public class TransactionTransformer extends Transformer {
case MESSAGE:
return MessageTransactionTransformer.fromByteBuffer(byteBuffer);
+ case DEPLOY_AT:
+ return DeployATTransactionTransformer.fromByteBuffer(byteBuffer);
+
default:
throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes");
}
@@ -150,6 +153,9 @@ public class TransactionTransformer extends Transformer {
case MESSAGE:
return MessageTransactionTransformer.getDataLength(transactionData);
+ case DEPLOY_AT:
+ return DeployATTransactionTransformer.getDataLength(transactionData);
+
default:
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] when requesting byte length");
}
@@ -205,6 +211,9 @@ public class TransactionTransformer extends Transformer {
case MESSAGE:
return MessageTransactionTransformer.toBytes(transactionData);
+ case DEPLOY_AT:
+ return DeployATTransactionTransformer.toBytes(transactionData);
+
default:
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes");
}
@@ -269,6 +278,9 @@ public class TransactionTransformer extends Transformer {
case MESSAGE:
return MessageTransactionTransformer.toBytesForSigningImpl(transactionData);
+ case DEPLOY_AT:
+ return DeployATTransactionTransformer.toBytesForSigningImpl(transactionData);
+
default:
throw new TransformationException(
"Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing");
@@ -345,6 +357,9 @@ public class TransactionTransformer extends Transformer {
case MESSAGE:
return MessageTransactionTransformer.toJSON(transactionData);
+ case DEPLOY_AT:
+ return DeployATTransactionTransformer.toJSON(transactionData);
+
default:
throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to JSON");
}
diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java
index 959c42af..c906ff87 100644
--- a/src/utils/Serialization.java
+++ b/src/utils/Serialization.java
@@ -17,6 +17,32 @@ public class Serialization {
/**
* Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to specified length.
*
+ * @param amount
+ * @param length
+ * @return byte[]
+ * @throws IOException
+ */
+ public static byte[] serializeBigDecimal(BigDecimal amount, int length) throws IOException {
+ byte[] amountBytes = amount.unscaledValue().toByteArray();
+ byte[] output = new byte[length];
+ System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length);
+ return output;
+ }
+
+ /**
+ * Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to fixed length of 8.
+ *
+ * @param amount
+ * @return byte[]
+ * @throws IOException
+ */
+ public static byte[] serializeBigDecimal(BigDecimal amount) throws IOException {
+ return serializeBigDecimal(amount, 8);
+ }
+
+ /**
+ * Write to ByteBuffer a BigDecimal, unscaled, prepended with zero bytes to specified length.
+ *
* @param ByteArrayOutputStream
* @param amount
* @param length
@@ -30,7 +56,7 @@ public class Serialization {
}
/**
- * Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to fixed length of 8.
+ * Write to ByteBuffer a BigDecimal, unscaled, prepended with zero bytes to fixed length of 8.
*
* @param ByteArrayOutputStream
* @param amount
@@ -73,9 +99,6 @@ public class Serialization {
}
public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException {
- if (byteBuffer.remaining() < Transformer.INT_LENGTH)
- throw new TransformationException("Byte data too short for serialized string size");
-
int size = byteBuffer.getInt();
if (size > maxSize)
throw new TransformationException("Serialized string too long");