diff --git a/src/data/account/Account.java b/src/data/account/AccountData.java similarity index 73% rename from src/data/account/Account.java rename to src/data/account/AccountData.java index af480bea..9f3c39d8 100644 --- a/src/data/account/Account.java +++ b/src/data/account/AccountData.java @@ -1,6 +1,6 @@ package data.account; -public class Account { +public class AccountData { // Properties protected String address; @@ -8,10 +8,10 @@ public class Account { // Constructors - protected Account() { + protected AccountData() { } - public Account(String address) { + public AccountData(String address) { this.address = address; } @@ -33,10 +33,10 @@ public class Account { @Override public boolean equals(Object b) { - if (!(b instanceof Account)) + if (!(b instanceof AccountData)) return false; - return this.getAddress().equals(((Account) b).getAddress()); + return this.getAddress().equals(((AccountData) b).getAddress()); } @Override diff --git a/src/data/account/GenesisAccount.java b/src/data/account/GenesisAccount.java deleted file mode 100644 index fdd0c4bb..00000000 --- a/src/data/account/GenesisAccount.java +++ /dev/null @@ -1,9 +0,0 @@ -package data.account; - -public final class GenesisAccount extends PublicKeyAccount { - - public GenesisAccount() { - super(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }); - } - -} diff --git a/src/data/account/PublicKeyAccount.java b/src/data/account/PublicKeyAccount.java deleted file mode 100644 index fd554d7e..00000000 --- a/src/data/account/PublicKeyAccount.java +++ /dev/null @@ -1,27 +0,0 @@ -package data.account; - -import qora.crypto.Crypto; - -public class PublicKeyAccount extends Account { - - // Properties - protected byte[] publicKey; - - // Constructors - - public PublicKeyAccount(byte[] publicKey) { - super(Crypto.toAddress(publicKey)); - - this.publicKey = publicKey; - } - - protected PublicKeyAccount() { - } - - // Getters/Setters - - public byte[] getPublicKey() { - return this.publicKey; - } - -} diff --git a/src/data/block/Block.java b/src/data/block/Block.java deleted file mode 100644 index b75d4c88..00000000 --- a/src/data/block/Block.java +++ /dev/null @@ -1,86 +0,0 @@ -package data.block; - -import java.math.BigDecimal; - -import qora.account.PublicKeyAccount; - -public class Block implements BlockData { - private int version; - private byte[] reference; - private int transactionCount; - private BigDecimal totalFees; - private byte[] transactionsSignature; - private int height; - private long timestamp; - private BigDecimal generatingBalance; - private byte[] generatorPublicKey; - private byte[] generatorSignature; - private byte[] atBytes; - private BigDecimal atFees; - - public Block(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, - int height, long timestamp, BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, - byte[] atBytes, BigDecimal atFees) - { - this.version = version; - this.reference = reference; - this.transactionCount = transactionCount; - this.totalFees = totalFees; - this.transactionsSignature = transactionsSignature; - this.height = height; - this.timestamp = timestamp; - this.generatingBalance = generatingBalance; - this.generatorPublicKey = generatorPublicKey; - this.generatorSignature = generatorSignature; - this.atBytes = atBytes; - this.atFees = atFees; - } - - public int getVersion() { - return version; - } - - public byte[] getReference() { - return reference; - } - - public int getTransactionCount() { - return transactionCount; - } - - public BigDecimal getTotalFees() { - return totalFees; - } - - public byte[] getTransactionsSignature() { - return transactionsSignature; - } - - public int getHeight() { - return height; - } - - public long getTimestamp() { - return timestamp; - } - - public BigDecimal getGeneratingBalance() { - return generatingBalance; - } - - public byte[] getGeneratorPublicKey() { - return generatorPublicKey; - } - - public byte[] getGeneratorSignature() { - return generatorSignature; - } - - public byte[] getAtBytes() { - return atBytes; - } - - public BigDecimal getAtFees() { - return atFees; - } -} diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index 20db7a94..33d2bd20 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -2,17 +2,119 @@ package data.block; import java.math.BigDecimal; -public interface BlockData { - public int getVersion(); - public byte[] getReference(); - public int getTransactionCount(); - public BigDecimal getTotalFees(); - public byte[] getTransactionsSignature(); - public int getHeight(); - public long getTimestamp(); - public BigDecimal getGeneratingBalance(); - public byte[] getGeneratorPublicKey(); - public byte[] getGeneratorSignature(); - public byte[] getAtBytes(); - public BigDecimal getAtFees(); +import com.google.common.primitives.Bytes; + +public class BlockData { + + private byte[] signature; + private int version; + private byte[] reference; + private int transactionCount; + private BigDecimal totalFees; + private byte[] transactionsSignature; + private int height; + private long timestamp; + private BigDecimal generatingBalance; + private byte[] generatorPublicKey; + private byte[] generatorSignature; + private byte[] atBytes; + private BigDecimal atFees; + + public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, int height, long timestamp, + BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, byte[] atBytes, BigDecimal atFees) { + this.version = version; + this.reference = reference; + this.transactionCount = transactionCount; + this.totalFees = totalFees; + this.transactionsSignature = transactionsSignature; + this.height = height; + this.timestamp = timestamp; + this.generatingBalance = generatingBalance; + this.generatorPublicKey = generatorPublicKey; + this.generatorSignature = generatorSignature; + this.atBytes = atBytes; + this.atFees = atFees; + + if (this.generatorSignature != null && this.transactionsSignature != null) + this.signature = Bytes.concat(this.generatorSignature, this.transactionsSignature); + else + this.signature = null; + } + + public int getTransactionCount() { + return this.transactionCount; + } + + public void setTransactionCount(int transactionCount) { + this.transactionCount = transactionCount; + } + + public BigDecimal getTotalFees() { + return this.totalFees; + } + + public void setTotalFees(BigDecimal totalFees) { + this.totalFees = totalFees; + } + + public byte[] getTransactionsSignature() { + return this.transactionsSignature; + } + + public void setTransactionsSignature(byte[] transactionsSignature) { + this.transactionsSignature = transactionsSignature; + } + + public byte[] getSignature() { + return this.signature; + } + + public int getVersion() { + return this.version; + } + + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + + public int getHeight() { + return this.height; + } + + public void setHeight(int height) { + this.height = height; + } + + public long getTimestamp() { + return this.timestamp; + } + + public BigDecimal getGeneratingBalance() { + return this.generatingBalance; + } + + public byte[] getGeneratorPublicKey() { + return this.generatorPublicKey; + } + + public byte[] getGeneratorSignature() { + return this.generatorSignature; + } + + public void setGeneratorSignature(byte[] generatorSignature) { + this.generatorSignature = generatorSignature; + } + + public byte[] getAtBytes() { + return this.atBytes; + } + + public BigDecimal getAtFees() { + return this.atFees; + } + } diff --git a/src/data/transaction/GenesisTransaction.java b/src/data/transaction/GenesisTransaction.java deleted file mode 100644 index 7c0f84e5..00000000 --- a/src/data/transaction/GenesisTransaction.java +++ /dev/null @@ -1,37 +0,0 @@ -package data.transaction; - -import java.math.BigDecimal; - -import data.account.Account; -import data.account.GenesisAccount; - -public class GenesisTransaction extends Transaction { - - // Properties - private Account recipient; - private BigDecimal amount; - - // Constructors - - public GenesisTransaction(Account recipient, BigDecimal amount, long timestamp, byte[] signature) { - super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount(), timestamp, signature); - - this.recipient = recipient; - this.amount = amount; - } - - public GenesisTransaction(Account recipient, BigDecimal amount, long timestamp) { - this(recipient, amount, timestamp, null); - } - - // Getters/Setters - - public Account getRecipient() { - return this.recipient; - } - - public BigDecimal getAmount() { - return this.amount; - } - -} diff --git a/src/data/transaction/GenesisTransactionData.java b/src/data/transaction/GenesisTransactionData.java new file mode 100644 index 00000000..ce813bf3 --- /dev/null +++ b/src/data/transaction/GenesisTransactionData.java @@ -0,0 +1,37 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.account.GenesisAccount; +import qora.transaction.Transaction.TransactionType; + +public class GenesisTransactionData extends TransactionData { + + // Properties + private String recipient; + private BigDecimal amount; + + // Constructors + + public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) { + super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount().getPublicKey(), timestamp, signature); + + this.recipient = recipient; + this.amount = amount; + } + + public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp) { + this(recipient, amount, timestamp, null); + } + + // Getters/Setters + + public String getRecipient() { + return this.recipient; + } + + public BigDecimal getAmount() { + return this.amount; + } + +} diff --git a/src/data/transaction/Transaction.java b/src/data/transaction/Transaction.java deleted file mode 100644 index 4d722664..00000000 --- a/src/data/transaction/Transaction.java +++ /dev/null @@ -1,83 +0,0 @@ -package data.transaction; - -import java.math.BigDecimal; -import java.util.Map; - -import data.account.PublicKeyAccount; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -public abstract class Transaction { - - // Transaction types - // TODO Transaction types are semantic and should go into the business logic layer. - // No need to know the meaning of the integer value in data layer - 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); - - public final int value; - - private final static Map map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type)); - - TransactionType(int value) { - this.value = value; - } - - public static TransactionType valueOf(int value) { - return map.get(value); - } - } - - // Properties shared with all transaction types - protected TransactionType type; - // TODO PublicKeyAccount is a separate data entity, so here should only be a key to reference it - protected PublicKeyAccount creator; - protected long timestamp; - protected byte[] reference; - protected BigDecimal fee; - protected byte[] signature; - - // Constructors - - public Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference, byte[] signature) { - this.fee = fee; - this.type = type; - this.creator = creator; - this.timestamp = timestamp; - this.reference = reference; - this.signature = signature; - } - - public Transaction(TransactionType type, BigDecimal fee, PublicKeyAccount creator, long timestamp, byte[] reference) { - this(type, fee, creator, timestamp, reference, null); - } - - // Getters/setters - - public TransactionType getType() { - return this.type; - } - - public PublicKeyAccount getCreator() { - return this.creator; - } - - public long getTimestamp() { - return this.timestamp; - } - - public byte[] getReference() { - return this.reference; - } - - public BigDecimal getFee() { - return this.fee; - } - - public byte[] getSignature() { - return this.signature; - } - -} diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java new file mode 100644 index 00000000..7d9753f9 --- /dev/null +++ b/src/data/transaction/TransactionData.java @@ -0,0 +1,58 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public abstract class TransactionData { + + // Properties shared with all transaction types + protected TransactionType type; + protected byte[] creatorPublicKey; + protected long timestamp; + protected byte[] reference; + protected BigDecimal fee; + protected byte[] signature; + + // Constructors + + public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) { + this.fee = fee; + this.type = type; + this.creatorPublicKey = creatorPublicKey; + this.timestamp = timestamp; + this.reference = reference; + this.signature = signature; + } + + public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference) { + this(type, fee, creatorPublicKey, timestamp, reference, null); + } + + // Getters/setters + + public TransactionType getType() { + return this.type; + } + + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; + } + + public long getTimestamp() { + return this.timestamp; + } + + public byte[] getReference() { + return this.reference; + } + + public BigDecimal getFee() { + return this.fee; + } + + public byte[] getSignature() { + return this.signature; + } + +} diff --git a/src/qora/account/PublicKeyAccount.java b/src/qora/account/PublicKeyAccount.java index 166787fb..121cfeb2 100644 --- a/src/qora/account/PublicKeyAccount.java +++ b/src/qora/account/PublicKeyAccount.java @@ -8,8 +8,9 @@ public class PublicKeyAccount extends Account { protected byte[] publicKey; public PublicKeyAccount(byte[] publicKey) { + super(Crypto.toAddress(publicKey)); + this.publicKey = publicKey; - this.address = Crypto.toAddress(this.publicKey); } protected PublicKeyAccount() { diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java index efe207ee..450401b5 100644 --- a/src/qora/assets/Asset.java +++ b/src/qora/assets/Asset.java @@ -6,17 +6,8 @@ import java.sql.SQLException; import database.DB; import database.NoDataFoundException; import qora.account.Account; -import qora.transaction.TransactionHandler; import repository.hsqldb.HSQLDBSaver; -/* - * TODO: - * Probably need to standardize on using assetId or assetKey for the long value, and plain "asset" for the java object. - * Thus in the database the primary key column could be called "asset_id". - * In the Order object, we'd pass longs to variables with names like "haveAssetId" and use getters like "getHaveAssetId" - * which frees up other method names like "getHaveAsset" to return a java Asset object. - */ - public class Asset { public static final long QORA = 0L; @@ -91,7 +82,7 @@ public class Asset { this.description = rs.getString(3); this.quantity = rs.getLong(4); this.isDivisible = rs.getBoolean(5); - this.reference = DB.getResultSetBytes(rs.getBinaryStream(6), TransactionHandler.REFERENCE_LENGTH); + this.reference = DB.getResultSetBytes(rs.getBinaryStream(6)); } public static Asset fromAssetId(long assetId) throws SQLException { diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index c66220c3..a107f573 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -22,6 +22,8 @@ import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; +import data.block.BlockData; +import data.transaction.TransactionData; import database.DB; import database.NoDataFoundException; import qora.account.PrivateKeyAccount; @@ -31,10 +33,14 @@ import qora.assets.Order; import qora.assets.Trade; import qora.transaction.CreateOrderTransaction; import qora.transaction.GenesisTransaction; -import qora.transaction.TransactionHandler; +import qora.transaction.Transaction; +import repository.BlockRepository; +import repository.DataException; +import repository.RepositoryManager; import repository.hsqldb.HSQLDBSaver; -import qora.transaction.TransactionHandler; import transform.TransformationException; +import transform.block.BlockTransformer; +import transform.transaction.TransactionTransformer; import utils.Base58; import utils.NTP; import utils.Serialization; @@ -63,51 +69,16 @@ import utils.Serialization; public class Block { - /** - * Ordered list of columns when fetching a Block row from database. - */ - private static final String DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees"; - - // Database properties - protected int version; - protected byte[] reference; - protected int transactionCount; - protected BigDecimal totalFees; - protected byte[] transactionsSignature; - protected int height; - protected long timestamp; - protected BigDecimal generatingBalance; - protected PublicKeyAccount generator; - protected byte[] generatorSignature; - protected byte[] atBytes; - protected BigDecimal atFees; - + // Properties + private BlockData blockData; + private PublicKeyAccount generator; + // Other properties - protected List transactions; + protected List transactions; protected BigDecimal cachedNextGeneratingBalance; - // Property lengths for serialisation - protected static final int VERSION_LENGTH = 4; - protected static final int TRANSACTIONS_SIGNATURE_LENGTH = 64; - protected static final int GENERATOR_SIGNATURE_LENGTH = 64; - protected static final int REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; - protected static final int TIMESTAMP_LENGTH = 8; - protected static final int GENERATING_BALANCE_LENGTH = 8; - protected static final int GENERATOR_LENGTH = 32; - protected static final int TRANSACTION_COUNT_LENGTH = 4; - protected static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH - + TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH; - - // Other length constants - protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; - public static final int MAX_BLOCK_BYTES = 1048576; - protected static final int TRANSACTION_SIZE_LENGTH = 4; // per transaction - protected static final int AT_BYTES_LENGTH = 4; - protected static final int AT_FEES_LENGTH = 8; - protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; - // Other useful constants + public static final int MAX_BLOCK_BYTES = 1048576; /** * Number of blocks between recalculating block's generating balance. */ @@ -125,91 +96,15 @@ public class Block { // Constructors - // For creating a new block from scratch - public Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] atBytes, BigDecimal atFees) { - this.version = version; - this.reference = reference; - this.timestamp = timestamp; - this.generatingBalance = generatingBalance; - this.generator = generator; - this.generatorSignature = null; - this.height = 0; - - this.transactionCount = 0; - this.transactions = new ArrayList(); - this.transactionsSignature = null; - this.totalFees = BigDecimal.ZERO.setScale(8); - - this.atBytes = atBytes; - this.atFees = atFees; - if (this.atFees != null) - this.totalFees = this.totalFees.add(this.atFees); - } - - // For instantiating a block that was previously serialized - protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature, - byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List transactions) { - this(version, reference, timestamp, generatingBalance, generator, atBytes, atFees); - - this.generatorSignature = generatorSignature; - - this.transactionsSignature = transactionsSignature; - this.transactionCount = transactions.size(); - this.transactions = transactions; - - // Add transactions' fees to totalFees - for (TransactionHandler transaction : this.transactions) - this.totalFees = this.totalFees.add(transaction.getFee()); + public Block(BlockData blockData) { + this.blockData = blockData; + this.generator = new PublicKeyAccount(blockData.getGeneratorPublicKey()); } // Getters/setters - public int getVersion() { - return this.version; - } - - public byte[] getReference() { - return this.reference; - } - - public long getTimestamp() { - return this.timestamp; - } - - public BigDecimal getGeneratingBalance() { - return this.generatingBalance; - } - - public PublicKeyAccount getGenerator() { - return this.generator; - } - - public byte[] getGeneratorSignature() { - return this.generatorSignature; - } - - public byte[] getTransactionsSignature() { - return this.transactionsSignature; - } - - public BigDecimal getTotalFees() { - return this.totalFees; - } - - public int getTransactionCount() { - return this.transactionCount; - } - - public byte[] getATBytes() { - return this.atBytes; - } - - public BigDecimal getATFees() { - return this.atFees; - } - - public int getHeight() { - return this.height; + public BlockData getBlockData() { + return this.blockData; } // More information @@ -220,26 +115,10 @@ public class Block { * @return byte[], or null if either component signature is null. */ public byte[] getSignature() { - if (this.generatorSignature == null || this.transactionsSignature == null) + if (this.blockData.getGeneratorSignature() == null || this.blockData.getTransactionsSignature() == null) return null; - return Bytes.concat(this.generatorSignature, this.transactionsSignature); - } - - public int getDataLength() { - int blockLength = BASE_LENGTH; - - if (version >= 2 && this.atBytes != null) - blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + this.atBytes.length; - - // Short cut for no transactions - if (this.transactions == null || this.transactions.isEmpty()) - return blockLength; - - for (TransactionHandler transaction : this.transactions) - blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength(); - - return blockLength; + return Bytes.concat(this.blockData.getGeneratorSignature(), this.blockData.getTransactionsSignature()); } /** @@ -248,9 +127,9 @@ public class Block { * @return 1, 2 or 3 */ public int getNextBlockVersion() { - if (this.height < AT_BLOCK_HEIGHT_RELEASE) + if (this.blockData.getHeight() < AT_BLOCK_HEIGHT_RELEASE) return 1; - else if (this.timestamp < POWFIX_RELEASE_TIMESTAMP) + else if (this.blockData.getTimestamp() < POWFIX_RELEASE_TIMESTAMP) return 2; else return 3; @@ -269,8 +148,8 @@ public class Block { */ public BigDecimal getNextBlockGeneratingBalance() throws SQLException { // This block not at the start of an interval? - if (this.height % BLOCK_RETARGET_INTERVAL != 0) - return this.generatingBalance; + if (this.blockData.getHeight() % BLOCK_RETARGET_INTERVAL != 0) + return this.blockData.getGeneratingBalance(); // Return cached calculation if we have one if (this.cachedNextGeneratingBalance != null) @@ -280,23 +159,29 @@ public class Block { // Navigate back to first block in previous interval: // XXX: why can't we simply load using block height? - Block firstBlock = this; - for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i) - firstBlock = firstBlock.getParent(); + BlockRepository blockRepo = RepositoryManager.getBlockRepository(); + BlockData firstBlock = this.blockData; + + try { + for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i) + firstBlock = blockRepo.fromSignature(firstBlock.getReference()); + } catch (DataException e) { + firstBlock = null; + } // Couldn't navigate back far enough? if (firstBlock == null) throw new IllegalStateException("Failed to calculate next block's generating balance due to lack of historic blocks"); // Calculate the actual time period (in ms) over previous interval's blocks. - long previousGeneratingTime = this.timestamp - firstBlock.getTimestamp(); + long previousGeneratingTime = this.blockData.getTimestamp() - firstBlock.getTimestamp(); // Calculate expected forging time (in ms) for a whole interval based on this block's generating balance. - long expectedGeneratingTime = Block.calcForgingDelay(this.generatingBalance) * BLOCK_RETARGET_INTERVAL * 1000; + long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BLOCK_RETARGET_INTERVAL * 1000; // Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances. BigDecimal multiplier = BigDecimal.valueOf((double) expectedGeneratingTime / (double) previousGeneratingTime); - this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.generatingBalance.multiply(multiplier)); + this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.blockData.getGeneratingBalance().multiply(multiplier)); return this.cachedNextGeneratingBalance; } @@ -316,310 +201,31 @@ public class Block { /** * Return block's transactions. *

- * If the block was loaded from DB then it's possible this method will call the DB to load the transactions if they are not already loaded. + * If the block was loaded from repository then it's possible this method will call the repository to load the transactions if they are not already loaded. * * @return - * @throws SQLException + * @throws DataException */ - public List getTransactions() throws SQLException { + public List getTransactions() throws DataException { // Already loaded? if (this.transactions != null) return this.transactions; // Allocate cache for results - this.transactions = new ArrayList(); + List transactionsData = RepositoryManager.getBlockRepository().getTransactionsFromSignature(this.blockData.getSignature()); - ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature()); - if (rs == null) - return this.transactions; // No transactions in this block - - // NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us - do { - byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH); - this.transactions.add(TransactionFactory.fromSignature(transactionSignature)); - - // No need to update totalFees as this will be loaded via the Blocks table - } while (rs.next()); - - // The number of transactions fetched from database should correspond with Block's transactionCount - if (this.transactions.size() != this.transactionCount) - throw new IllegalStateException("Block's transactions from database do not match block's transaction count"); + // The number of transactions fetched from repository should correspond with Block's transactionCount + if (transactionsData.size() != this.blockData.getTransactionCount()) + throw new IllegalStateException("Block's transactions from repository do not match block's transaction count"); + this.transactions = new ArrayList(); + + for (TransactionData transactionData : transactionsData) + this.transactions.add(Transaction.fromData(transactionData)); + return this.transactions; } - // Load/Save - - protected Block(byte[] signature) throws SQLException { - this(DB.checkedExecute("SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature)); - } - - protected Block(ResultSet rs) throws SQLException { - if (rs == null) - throw new NoDataFoundException(); - - this.version = rs.getInt(1); - this.reference = DB.getResultSetBytes(rs.getBinaryStream(2), REFERENCE_LENGTH); - this.transactionCount = rs.getInt(3); - this.totalFees = rs.getBigDecimal(4); - this.transactionsSignature = DB.getResultSetBytes(rs.getBinaryStream(5), TRANSACTIONS_SIGNATURE_LENGTH); - this.height = rs.getInt(6); - this.timestamp = rs.getTimestamp(7).getTime(); - this.generatingBalance = rs.getBigDecimal(8); - // Note: can't use GENERATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key - this.generator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(9))); - this.generatorSignature = DB.getResultSetBytes(rs.getBinaryStream(10), GENERATOR_SIGNATURE_LENGTH); - this.atBytes = DB.getResultSetBytes(rs.getBinaryStream(11)); - this.atFees = rs.getBigDecimal(12); - } - - /** - * Load Block from DB using block signature. - * - * @param signature - * @return Block, or null if not found - * @throws SQLException - */ - public static Block fromSignature(byte[] signature) throws SQLException { - try { - return new Block(signature); - } catch (NoDataFoundException e) { - return null; - } - } - - /** - * Load Block from DB using block height - * - * @param height - * @return Block, or null if not found - * @throws SQLException - */ - public static Block fromHeight(int height) throws SQLException { - PreparedStatement preparedStatement = DB.getConnection().prepareStatement("SELECT " + DB_COLUMNS + " FROM Blocks WHERE height = ?"); - preparedStatement.setInt(1, height); - - try { - return new Block(DB.checkedExecute(preparedStatement)); - } catch (NoDataFoundException e) { - return null; - } - } - - protected void save() throws SQLException { - HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks"); - - saveHelper.bind("signature", this.getSignature()).bind("version", this.version).bind("reference", this.reference) - .bind("transaction_count", this.transactionCount).bind("total_fees", this.totalFees).bind("transactions_signature", this.transactionsSignature) - .bind("height", this.height).bind("generation", new Timestamp(this.timestamp)).bind("generating_balance", this.generatingBalance) - .bind("generator", this.generator.getPublicKey()).bind("generator_signature", this.generatorSignature).bind("AT_data", this.atBytes) - .bind("AT_fees", this.atFees); - - saveHelper.execute(); - } - - // Navigation - - /** - * Load parent Block from DB - * - * @return Block, or null if not found - * @throws SQLException - */ - public Block getParent() throws SQLException { - try { - return new Block(this.reference); - } catch (NoDataFoundException e) { - return null; - } - } - - /** - * Load child Block from DB - * - * @return Block, or null if not found - * @throws SQLException - */ - public Block getChild() throws SQLException { - byte[] blockSignature = this.getSignature(); - if (blockSignature == null) - return null; - - ResultSet resultSet = DB.checkedExecute("SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature); - - try { - return new Block(resultSet); - } catch (NoDataFoundException e) { - return null; - } - } - - // Converters - - @SuppressWarnings("unchecked") - public JSONObject toJSON() throws SQLException { - JSONObject json = new JSONObject(); - - json.put("version", this.version); - json.put("timestamp", this.timestamp); - json.put("generatingBalance", this.generatingBalance); - json.put("generator", this.generator.getAddress()); - json.put("generatorPublicKey", Base58.encode(this.generator.getPublicKey())); - json.put("fee", this.getTotalFees().toPlainString()); - json.put("transactionsSignature", Base58.encode(this.transactionsSignature)); - json.put("generatorSignature", Base58.encode(this.generatorSignature)); - json.put("signature", Base58.encode(this.getSignature())); - - if (this.reference != null) - json.put("reference", Base58.encode(this.reference)); - - json.put("height", this.getHeight()); - - // Add transaction info - JSONArray transactionsJson = new JSONArray(); - boolean tradesHappened = false; - - for (TransactionHandler transaction : this.getTransactions()) { - transactionsJson.add(transaction.toJSON()); - - // If this is an asset CreateOrderTransaction then check to see if any trades happened - if (transaction.getType() == Transaction.TransactionHandler.CREATE_ASSET_ORDER) { - CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction; - Order order = orderTransaction.getOrder(); - List trades = order.getTrades(); - - // Filter out trades with timestamps that don't match order transaction's timestamp - trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp()); - - // Any trades left? - if (!trades.isEmpty()) { - tradesHappened = true; - - // No need to check any further - break; - } - } - } - json.put("transactions", transactionsJson); - - // Add asset trade activity flag - json.put("assetTrades", tradesHappened); - - // Add CIYAM AT info (if any) - if (atBytes != null) { - json.put("blockATs", HashCode.fromBytes(atBytes).toString()); - json.put("atFees", this.atFees); - } - - return json; - } - - public byte[] toBytes() throws SQLException { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength()); - bytes.write(Ints.toByteArray(this.version)); - bytes.write(Longs.toByteArray(this.timestamp)); - bytes.write(this.reference); - // NOTE: generatingBalance serialized as long value, not as BigDecimal, for historic compatibility - bytes.write(Longs.toByteArray(this.generatingBalance.longValue())); - bytes.write(this.generator.getPublicKey()); - bytes.write(this.transactionsSignature); - bytes.write(this.generatorSignature); - - if (this.version >= 2) { - if (this.atBytes != null) { - bytes.write(Ints.toByteArray(this.atBytes.length)); - bytes.write(this.atBytes); - // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility - bytes.write(Longs.toByteArray(this.atFees.longValue())); - } else { - bytes.write(Ints.toByteArray(0)); - bytes.write(Longs.toByteArray(0L)); - } - } - - // Transactions - bytes.write(Ints.toByteArray(this.transactionCount)); - - for (TransactionHandler transaction : this.getTransactions()) { - bytes.write(Ints.toByteArray(transaction.getDataLength())); - bytes.write(transaction.toBytes()); - } - - return bytes.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static Block parse(byte[] data) throws TransformationException { - if (data == null) - return null; - - if (data.length < BASE_LENGTH) - throw new TransformationException("Byte data too short for Block"); - - ByteBuffer byteBuffer = ByteBuffer.wrap(data); - - int version = byteBuffer.getInt(); - - if (version >= 2 && data.length < BASE_LENGTH + AT_LENGTH) - throw new TransformationException("Byte data too short for V2+ Block"); - - long timestamp = byteBuffer.getLong(); - - byte[] reference = new byte[REFERENCE_LENGTH]; - byteBuffer.get(reference); - - BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8); - PublicKeyAccount generator = Serialization.deserializePublicKey(byteBuffer); - - byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH]; - byteBuffer.get(transactionsSignature); - byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH]; - byteBuffer.get(generatorSignature); - - byte[] atBytes = null; - BigDecimal atFees = null; - if (version >= 2) { - int atBytesLength = byteBuffer.getInt(); - - if (atBytesLength > MAX_BLOCK_BYTES) - throw new TransformationException("Byte data too long for Block's AT info"); - - atBytes = new byte[atBytesLength]; - byteBuffer.get(atBytes); - - atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8); - } - - int transactionCount = byteBuffer.getInt(); - - // Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be - List transactions = new ArrayList(); - for (int t = 0; t < transactionCount; ++t) { - if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH) - throw new TransformationException("Byte data too short for Block Transaction length"); - - int transactionLength = byteBuffer.getInt(); - if (byteBuffer.remaining() < transactionLength) - throw new TransformationException("Byte data too short for Block Transaction"); - if (transactionLength > MAX_BLOCK_BYTES) - throw new TransformationException("Byte data too long for Block Transaction"); - - byte[] transactionBytes = new byte[transactionLength]; - byteBuffer.get(transactionBytes); - - TransactionHandler transaction = TransactionHandler.parse(transactionBytes); - transactions.add(transaction); - } - - if (byteBuffer.hasRemaining()) - throw new TransformationException("Excess byte data found after parsing Block"); - - return new Block(version, reference, timestamp, generatingBalance, generator, generatorSignature, transactionsSignature, atBytes, atFees, transactions); - } - // Processing /** @@ -629,12 +235,12 @@ public class Block { *

* Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated. * - * @param transaction + * @param transactionData * @return true if transaction successfully added to block, false otherwise * @throws IllegalStateException * if block's {@code generator} is not a {@code PrivateKeyAccount}. */ - public boolean addTransaction(TransactionHandler transaction) { + public boolean addTransaction(TransactionData transactionData) { // Can't add to transactions if we haven't loaded existing ones yet if (this.transactions == null) throw new IllegalStateException("Attempted to add transaction to partially loaded database Block"); @@ -643,17 +249,21 @@ public class Block { throw new IllegalStateException("Block's generator has no private key"); // Check there is space in block - if (this.getDataLength() + transaction.getDataLength() > MAX_BLOCK_BYTES) + try { + if (BlockTransformer.getDataLength(this.blockData) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES) + return false; + } catch (TransformationException e) { return false; + } // Add to block - this.transactions.add(transaction); + this.transactions.add(Transaction.fromData(transactionData)); // Update transaction count - this.transactionCount++; + this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); // Update totalFees - this.totalFees.add(transaction.getFee()); + this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee())); // Update transactions signature calcTransactionsSignature(); @@ -668,29 +278,17 @@ public class Block { * * @throws IllegalStateException * if block's {@code generator} is not a {@code PrivateKeyAccount}. + * @throws RuntimeException + * if somehow the generator signature cannot be calculated */ public void calcGeneratorSignature() { if (!(this.generator instanceof PrivateKeyAccount)) throw new IllegalStateException("Block's generator has no private key"); - this.generatorSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForGeneratorSignature()); - } - - private byte[] getBytesForGeneratorSignature() { try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); - - // Only copy the generator signature from reference, which is the first 64 bytes. - bytes.write(Arrays.copyOf(this.reference, GENERATOR_SIGNATURE_LENGTH)); - - bytes.write(Longs.toByteArray(this.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(this.generator.getPublicKey(), GENERATOR_LENGTH, 0)); - - return bytes.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); + this.blockData.setGeneratorSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForGeneratorSignature(this.blockData))); + } catch (TransformationException e) { + throw new RuntimeException("Unable to calculate block's generator signature", e); } } @@ -701,41 +299,33 @@ public class Block { * * @throws IllegalStateException * if block's {@code generator} is not a {@code PrivateKeyAccount}. + * @throws RuntimeException + * if somehow the transactions signature cannot be calculated */ public void calcTransactionsSignature() { if (!(this.generator instanceof PrivateKeyAccount)) throw new IllegalStateException("Block's generator has no private key"); - this.transactionsSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForTransactionsSignature()); - } - - private byte[] getBytesForTransactionsSignature() { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * TransactionHandler.SIGNATURE_LENGTH); - try { - bytes.write(this.generatorSignature); - - for (TransactionHandler transaction : this.getTransactions()) { - if (!transaction.isSignatureValid()) - return null; - - bytes.write(transaction.getSignature()); - } - - return bytes.toByteArray(); - } catch (IOException | SQLException e) { - throw new RuntimeException(e); + this.blockData.setTransactionsSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForTransactionsSignature(this))); + } catch (TransformationException e) { + throw new RuntimeException("Unable to calculate block's transactions signature", e); } } - public boolean isSignatureValid() { - // Check generator's signature first - if (!this.generator.verify(this.generatorSignature, getBytesForGeneratorSignature())) - return false; - // Check transactions signature - if (!this.generator.verify(this.transactionsSignature, getBytesForTransactionsSignature())) + public boolean isSignatureValid() { + try { + // Check generator's signature first + if (!this.generator.verify(this.blockData.getGeneratorSignature(), BlockTransformer.getBytesForGeneratorSignature(this.blockData))) + return false; + + // Check transactions signature + if (!this.generator.verify(this.blockData.getTransactionsSignature(), BlockTransformer.getBytesForTransactionsSignature(this))) + return false; + } catch (TransformationException e) { return false; + } return true; } @@ -749,41 +339,44 @@ public class Block { * * @return true if block is valid, false otherwise. * @throws SQLException + * @throws DataException */ - public boolean isValid() throws SQLException { + public boolean isValid() throws SQLException, DataException { // TODO // Check parent blocks exists - if (this.reference == null) + if (this.blockData.getReference() == null) return false; - Block parentBlock = this.getParent(); - if (parentBlock == null) + BlockData parentBlockData = RepositoryManager.getBlockRepository().fromSignature(this.blockData.getReference()); + if (parentBlockData == null) return false; + Block parentBlock = new Block(parentBlockData); + // Check timestamp is valid, i.e. later than parent timestamp and not in the future, within ~500ms margin - if (this.timestamp < parentBlock.getTimestamp() || this.timestamp - BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) + if (this.blockData.getTimestamp() < parentBlockData.getTimestamp() || this.blockData.getTimestamp() - BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) return false; // Legacy gen1 test: check timestamp ms is the same as parent timestamp ms? - if (this.timestamp % 1000 != parentBlock.getTimestamp() % 1000) + if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000) return false; // Check block version - if (this.version != parentBlock.getNextBlockVersion()) + if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) return false; - if (this.version < 2 && (this.atBytes != null || this.atBytes.length > 0 || this.atFees != null || this.atFees.compareTo(BigDecimal.ZERO) > 0)) + if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null)) return false; // Check generating balance - if (this.generatingBalance != parentBlock.getNextBlockGeneratingBalance()) + if (this.blockData.getGeneratingBalance() != parentBlock.getNextBlockGeneratingBalance()) return false; // Check generator's proof of stake against block's generating balance // TODO // Check CIYAM AT - if (this.atBytes != null && this.atBytes.length > 0) { + if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) { // TODO // try { // AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1); @@ -796,18 +389,18 @@ public class Block { // Check transactions Savepoint savepoint = DB.createSavepoint("BLOCK_TRANSACTIONS"); try { - for (TransactionHandler transaction : this.getTransactions()) { + for (Transaction transaction : this.getTransactions()) { // GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them) if (transaction instanceof GenesisTransaction) return false; // Check timestamp and deadline - if (transaction.getTimestamp() > this.timestamp || transaction.getDeadline() <= this.timestamp) + if (transaction.getTransactionData().getTimestamp() > this.blockData.getTimestamp() || transaction.getDeadline() <= this.blockData.getTimestamp()) return false; // Check transaction is even valid // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid - if (transaction.isValid() != TransactionHandler.ValidationResult.OK) + if (transaction.isValid() != Transaction.ValidationResult.OK) return false; // Process transaction to make sure other transactions validate properly @@ -818,6 +411,8 @@ public class Block { return false; } } + } catch (DataException e) { + return false; } finally { // Revert back to savepoint try { @@ -834,32 +429,32 @@ public class Block { return true; } - public void process() throws SQLException { + public void process() throws DataException, SQLException { // Process transactions (we'll link them to this block after saving the block itself) - List transactions = this.getTransactions(); - for (TransactionHandler transaction : transactions) + List transactions = this.getTransactions(); + for (Transaction transaction : transactions) transaction.process(); // If fees are non-zero then add fees to generator's balance - BigDecimal blockFee = this.getTotalFees(); + BigDecimal blockFee = this.blockData.getTotalFees(); if (blockFee.compareTo(BigDecimal.ZERO) == 1) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); // Link block into blockchain by fetching signature of highest block and setting that as our reference int blockchainHeight = BlockChain.getHeight(); - Block latestBlock = Block.fromHeight(blockchainHeight); - if (latestBlock != null) - this.reference = latestBlock.getSignature(); + BlockData latestBlockData = RepositoryManager.getBlockRepository().fromHeight(blockchainHeight); + if (latestBlockData != null) + this.blockData.setReference(latestBlockData.getSignature()); - this.height = blockchainHeight + 1; - this.save(); + this.blockData.setHeight(blockchainHeight + 1); + RepositoryManager.getBlockRepository().save(this.blockData); // Link transactions to this block, thus removing them from unconfirmed transactions list. for (int sequence = 0; sequence < transactions.size(); ++sequence) { - TransactionHandler transaction = transactions.get(sequence); + Transaction transaction = transactions.get(sequence); // Link transaction to this block - BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getSignature()); + BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getTransactionData().getSignature()); blockTransaction.save(); } } diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 7ef9e233..9850fb59 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -87,14 +87,17 @@ public class BlockChain { * Return highest block height from DB. * * @return height, or 0 if there are no blocks in DB (not very likely). - * @throws SQLException */ - public static int getHeight() throws SQLException { - ResultSet rs = DB.checkedExecute("SELECT MAX(height) FROM Blocks"); - if (rs == null) - return 0; + public static int getHeight() { + try { + ResultSet rs = DB.checkedExecute("SELECT MAX(height) FROM Blocks"); + if (rs == null) + return 0; - return rs.getInt(1); + return rs.getInt(1); + } catch (SQLException e) { + return 0; + } } /** diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index cdf76253..af30ee93 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -5,9 +5,8 @@ import java.sql.SQLException; import database.DB; import database.NoDataFoundException; -import qora.transaction.TransactionHandler; +import qora.transaction.Transaction; import repository.hsqldb.HSQLDBSaver; -import qora.transaction.TransactionHandler; public class BlockTransaction { @@ -50,7 +49,7 @@ public class BlockTransaction { this.blockSignature = blockSignature; this.sequence = sequence; - this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), TransactionHandler.SIGNATURE_LENGTH); + this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1)); } protected BlockTransaction(byte[] transactionSignature) throws SQLException { @@ -58,7 +57,7 @@ public class BlockTransaction { if (rs == null) throw new NoDataFoundException(); - this.blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Block.BLOCK_SIGNATURE_LENGTH); + this.blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1)); this.sequence = rs.getInt(2); this.transactionSignature = transactionSignature; } @@ -118,8 +117,10 @@ public class BlockTransaction { * @return Transaction, or null if not found (which should never happen) * @throws SQLException */ - public TransactionHandler getTransaction() throws SQLException { - return TransactionFactory.fromSignature(this.transactionSignature); + public Transaction getTransaction() throws SQLException { + // XXX + // return TransactionFactory.fromSignature(this.transactionSignature); + return null; } } diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 88bd40c0..c5bd05b0 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -1,151 +1,21 @@ package qora.transaction; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.math.BigDecimal; -import java.nio.ByteBuffer; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Arrays; -import org.json.simple.JSONObject; - import com.google.common.primitives.Bytes; -import com.google.common.primitives.Ints; -import com.google.common.primitives.Longs; -import database.DB; -import database.NoDataFoundException; -import qora.account.Account; -import qora.account.GenesisAccount; +import data.transaction.GenesisTransactionData; +import data.transaction.TransactionData; import qora.account.PrivateKeyAccount; -import qora.assets.Asset; import qora.crypto.Crypto; -import repository.hsqldb.HSQLDBSaver; import transform.TransformationException; -import utils.Base58; -import utils.Serialization; +import transform.transaction.TransactionTransformer; -public class GenesisTransaction extends TransactionHandler { +public class GenesisTransaction extends Transaction { - // Properties - private Account recipient; - private BigDecimal amount; - - // Property lengths - private static final int RECIPIENT_LENGTH = 25; // raw, not Base58-encoded - private static final int AMOUNT_LENGTH = 8; - // Note that Genesis transactions don't require reference, fee or signature: - private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; - - // Constructors - - public GenesisTransaction(String recipient, BigDecimal amount, long timestamp) { - super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount(), timestamp, null, null); - - this.recipient = new Account(recipient); - this.amount = amount; - this.signature = calcSignature(); - } - - // Getters/Setters - - public Account getRecipient() { - return this.recipient; - } - - public BigDecimal getAmount() { - return this.amount; - } - - // More information - - @Override - public int getDataLength() { - return TYPE_LENGTH + TYPELESS_LENGTH; - } - - // Load/Save - - /** - * Load GenesisTransaction from DB using signature. - * - * @param signature - * @throws NoDataFoundException - * if no matching row found - * @throws SQLException - */ - protected GenesisTransaction(byte[] signature) throws SQLException { - super(TransactionType.GENESIS, signature); - - ResultSet rs = DB.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); - if (rs == null) - throw new NoDataFoundException(); - - this.recipient = new Account(rs.getString(1)); - this.amount = rs.getBigDecimal(2).setScale(8); - } - - /** - * Load GenesisTransaction from DB using signature - * - * @param signature - * @return GenesisTransaction, or null if not found - * @throws SQLException - */ - public static GenesisTransaction fromSignature(byte[] signature) throws SQLException { - try { - return new GenesisTransaction(signature); - } catch (NoDataFoundException e) { - return null; - } - } - - @Override - public void save() throws SQLException { - super.save(); - - HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions"); - saveHelper.bind("signature", this.signature).bind("recipient", this.recipient.getAddress()).bind("amount", this.amount); - saveHelper.execute(); - } - - // Converters - - protected static TransactionHandler parse(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for GenesisTransaction"); - - long timestamp = byteBuffer.getLong(); - String recipient = Serialization.deserializeRecipient(byteBuffer); - BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); - - return new GenesisTransaction(recipient, amount, timestamp); - } - - @SuppressWarnings("unchecked") - @Override - public JSONObject toJSON() throws SQLException { - JSONObject json = getBaseJSON(); - - json.put("recipient", this.recipient.getAddress()); - json.put("amount", this.amount.toPlainString()); - - return json; - } - - @Override - public byte[] toBytes() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength()); - bytes.write(Ints.toByteArray(this.type.value)); - bytes.write(Longs.toByteArray(this.timestamp)); - bytes.write(Base58.decode(this.recipient.getAddress())); - bytes.write(Serialization.serializeBigDecimal(this.amount)); - return bytes.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } + public GenesisTransaction(TransactionData transactionData) { + this.transactionData = transactionData; } // Processing @@ -174,8 +44,12 @@ public class GenesisTransaction extends TransactionHandler { * @return byte[] */ private byte[] calcSignature() { - byte[] digest = Crypto.digest(toBytes()); - return Bytes.concat(digest, digest); + try { + byte[] digest = Crypto.digest(TransactionTransformer.toBytes(this.transactionData)); + return Bytes.concat(digest, digest); + } catch (TransformationException e) { + return null; + } } /** @@ -189,42 +63,50 @@ public class GenesisTransaction extends TransactionHandler { */ @Override public boolean isSignatureValid() { - return Arrays.equals(this.signature, calcSignature()); + return Arrays.equals(this.transactionData.getSignature(), this.calcSignature()); } @Override public ValidationResult isValid() { + GenesisTransactionData genesisTransaction = (GenesisTransactionData) this.transactionData; + // Check amount is zero or positive - if (this.amount.compareTo(BigDecimal.ZERO) == -1) + if (genesisTransaction.getAmount().compareTo(BigDecimal.ZERO) == -1) return ValidationResult.NEGATIVE_AMOUNT; // Check recipient address is valid - if (!Crypto.isValidAddress(this.recipient.getAddress())) + if (!Crypto.isValidAddress(genesisTransaction.getRecipient())) return ValidationResult.INVALID_ADDRESS; return ValidationResult.OK; } @Override - public void process() throws SQLException { - this.save(); + public void process() { + // TODO + // this.save(); // Set recipient's balance - this.recipient.setConfirmedBalance(Asset.QORA, this.amount); + // TODO + // this.recipient.setConfirmedBalance(Asset.QORA, this.amount); // Set recipient's reference - recipient.setLastReference(this.signature); + // TODO + // recipient.setLastReference(this.signature); } @Override - public void orphan() throws SQLException { - this.delete(); + public void orphan() { + // TODO + // this.delete(); // Reset recipient's balance - this.recipient.deleteBalance(Asset.QORA); + // TODO + // this.recipient.deleteBalance(Asset.QORA); // Set recipient's reference - recipient.setLastReference(null); + // TODO + // recipient.setLastReference(null); } } diff --git a/src/qora/transaction/TransactionHandler.java b/src/qora/transaction/Transaction.java similarity index 75% rename from src/qora/transaction/TransactionHandler.java rename to src/qora/transaction/Transaction.java index 3353aee2..0bb31897 100644 --- a/src/qora/transaction/TransactionHandler.java +++ b/src/qora/transaction/Transaction.java @@ -7,25 +7,36 @@ import java.util.Map; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toMap; -import org.json.simple.JSONObject; - -import data.transaction.Transaction; -import database.DB; -import database.NoDataFoundException; +import data.block.BlockData; +import data.transaction.TransactionData; import qora.account.PrivateKeyAccount; -import qora.account.PublicKeyAccount; import qora.block.Block; import qora.block.BlockChain; -import qora.block.BlockTransaction; -import repository.Repository; import repository.RepositoryManager; import settings.Settings; import transform.TransformationException; import transform.Transformer; import transform.transaction.TransactionTransformer; -import utils.Base58; -public abstract class TransactionHandler { +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); + + public final int value; + + private final static Map map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type)); + + TransactionType(int value) { + this.value = value; + } + + public static TransactionType valueOf(int value) { + return map.get(value); + } + } // Validation results public enum ValidationResult { @@ -52,30 +63,40 @@ public abstract class TransactionHandler { protected static final BigDecimal maxBytePerFee = BigDecimal.valueOf(Settings.getInstance().getMaxBytePerFee()); protected static final BigDecimal minFeePerByte = BigDecimal.ONE.divide(maxBytePerFee, MathContext.DECIMAL32); - private Transaction transaction; + protected TransactionData transactionData; // Constructors - public TransactionHandler(Transaction transaction) { - this.transaction = transaction; + public static Transaction fromData(TransactionData transactionData) { + switch (transactionData.getType()) { + case GENESIS: + return new GenesisTransaction(transactionData); + + default: + return null; + } } + // Getters / Setters + + public TransactionData getTransactionData() { + return this.transactionData; + } + // More information - - public long getDeadline() { // 24 hour deadline to include transaction in a block - return this.transaction.getTimestamp() + (24 * 60 * 60 * 1000); + return this.transactionData.getTimestamp() + (24 * 60 * 60 * 1000); } public boolean hasMinimumFee() { - return this.transaction.getFee().compareTo(MINIMUM_FEE) >= 0; + return this.transactionData.getFee().compareTo(MINIMUM_FEE) >= 0; } public BigDecimal feePerByte() { try { - return this.transaction.getFee().divide(new BigDecimal(TransactionTransformer.getDataLength(this.transaction)), MathContext.DECIMAL32); + return this.transactionData.getFee().divide(new BigDecimal(TransactionTransformer.getDataLength(this.transactionData)), MathContext.DECIMAL32); } catch (TransformationException e) { throw new IllegalStateException("Unable to get transaction byte length?"); } @@ -87,7 +108,7 @@ public abstract class TransactionHandler { public BigDecimal calcRecommendedFee() { try { - BigDecimal recommendedFee = BigDecimal.valueOf(TransactionTransformer.getDataLength(this.transaction)).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8); + BigDecimal recommendedFee = BigDecimal.valueOf(TransactionTransformer.getDataLength(this.transactionData)).divide(maxBytePerFee, MathContext.DECIMAL32).setScale(8); // security margin recommendedFee = recommendedFee.add(new BigDecimal("0.0000001")); @@ -118,7 +139,7 @@ public abstract class TransactionHandler { * @return height, or 0 if not in blockchain (i.e. unconfirmed) */ public int getHeight() { - return RepositoryManager.getTransactionRepository().getHeight(this.transaction); + return RepositoryManager.getTransactionRepository().getHeight(this.transactionData); } /** @@ -132,6 +153,9 @@ public abstract class TransactionHandler { return 0; int blockChainHeight = BlockChain.getHeight(); + if (blockChainHeight == 0) + return 0; + return blockChainHeight - ourHeight + 1; } @@ -142,8 +166,8 @@ public abstract class TransactionHandler { * * @return Block, or null if transaction is not in a Block */ - public Block getBlock() { - return RepositoryManager.getTransactionRepository().toBlock(this.transaction); + public BlockData getBlock() { + return RepositoryManager.getTransactionRepository().toBlock(this.transactionData); } /** @@ -151,8 +175,8 @@ public abstract class TransactionHandler { * * @return Transaction, or null if no parent found (which should not happen) */ - public Transaction getParent() { - byte[] reference = this.transaction.getReference(); + public TransactionData getParent() { + byte[] reference = this.transactionData.getReference(); if (reference == null) return null; @@ -164,8 +188,8 @@ public abstract class TransactionHandler { * * @return Transaction, or null if no child found */ - public Transaction getChild() { - byte[] signature = this.transaction.getSignature(); + public TransactionData getChild() { + byte[] signature = this.transactionData.getSignature(); if (signature == null) return null; @@ -181,7 +205,7 @@ public abstract class TransactionHandler { */ private byte[] toBytesLessSignature() { try { - byte[] bytes = TransactionTransformer.toBytes(this.transaction); + byte[] bytes = TransactionTransformer.toBytes(this.transactionData); return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); } catch (TransformationException e) { // XXX this isn't good @@ -196,7 +220,7 @@ public abstract class TransactionHandler { } public boolean isSignatureValid() { - byte[] signature = this.transaction.getSignature(); + byte[] signature = this.transactionData.getSignature(); if (signature == null) return false; diff --git a/src/repository/BlockRepository.java b/src/repository/BlockRepository.java index b637a667..781e3b6d 100644 --- a/src/repository/BlockRepository.java +++ b/src/repository/BlockRepository.java @@ -1,8 +1,20 @@ package repository; +import java.util.List; + import data.block.BlockData; +import data.transaction.TransactionData; public interface BlockRepository { - BlockData fromSignature(byte[] signature) throws DataException; - BlockData fromHeight(int height) throws DataException; + + public BlockData fromSignature(byte[] signature) throws DataException; + + public BlockData fromReference(byte[] reference) throws DataException; + + public BlockData fromHeight(int height) throws DataException; + + public List getTransactionsFromSignature(byte[] signature) throws DataException; + + public void save(BlockData blockData) throws DataException; + } diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index 7ca1a15a..0a46c5d6 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -1,20 +1,20 @@ package repository; -import data.transaction.Transaction; -import qora.block.Block; +import data.transaction.TransactionData; +import data.block.BlockData; public interface TransactionRepository { - public Transaction fromSignature(byte[] signature); + public TransactionData fromSignature(byte[] signature); - public Transaction fromReference(byte[] reference); + public TransactionData fromReference(byte[] reference); - public int getHeight(Transaction transaction); + public int getHeight(TransactionData transaction); - public Block toBlock(Transaction transaction); + public BlockData toBlock(TransactionData transaction); - public void save(Transaction transaction); + public void save(TransactionData transaction) throws DataException; - public void delete(Transaction transaction); + public void delete(TransactionData transaction) throws DataException; } diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java index 16d8a043..64336867 100644 --- a/src/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/repository/hsqldb/HSQLDBBlockRepository.java @@ -1,20 +1,21 @@ package repository.hsqldb; import java.math.BigDecimal; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; -import data.block.Block; import data.block.BlockData; +import data.transaction.TransactionData; import database.DB; -import qora.account.PublicKeyAccount; import repository.BlockRepository; import repository.DataException; +import repository.RepositoryManager; +import repository.TransactionRepository; -public class HSQLDBBlockRepository implements BlockRepository -{ +public class HSQLDBBlockRepository implements BlockRepository { protected static final int TRANSACTIONS_SIGNATURE_LENGTH = 64; protected static final int GENERATOR_SIGNATURE_LENGTH = 64; protected static final int REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; @@ -27,30 +28,38 @@ public class HSQLDBBlockRepository implements BlockRepository public HSQLDBBlockRepository(HSQLDBRepository repository) { this.repository = repository; } - - public BlockData fromSignature(byte[] signature) throws DataException - { - ResultSet rs; + + public BlockData fromSignature(byte[] signature) throws DataException { try { - rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature); + ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature); + return getBlockFromResultSet(rs); } catch (SQLException e) { throw new DataException("Error loading data from DB", e); } - return getBlockFromResultSet(rs); } - public BlockData fromHeight(int height) throws DataException - { - ResultSet rs; + public BlockData fromReference(byte[] reference) throws DataException { try { - rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height); + ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height); + return getBlockFromResultSet(rs); + } catch (SQLException e) { + throw new DataException("Error loading data from DB", e); + } + } + + public BlockData fromHeight(int height) throws DataException { + try { + ResultSet rs = DB.checkedExecute(repository.connection, "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height); + return getBlockFromResultSet(rs); } catch (SQLException e) { throw new DataException("Error loading data from DB", e); } - return getBlockFromResultSet(rs); } private BlockData getBlockFromResultSet(ResultSet rs) throws DataException { + if (rs == null) + return null; + try { int version = rs.getInt(1); byte[] reference = DB.getResultSetBytes(rs.getBinaryStream(2)); @@ -65,12 +74,49 @@ public class HSQLDBBlockRepository implements BlockRepository byte[] atBytes = DB.getResultSetBytes(rs.getBinaryStream(11)); BigDecimal atFees = rs.getBigDecimal(12); - return new Block(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, - generatingBalance,generatorPublicKey, generatorSignature, atBytes, atFees); - } - catch(SQLException e) - { + return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, generatorPublicKey, + generatorSignature, atBytes, atFees); + } catch(SQLException e) { throw new DataException("Error extracting data from result set", e); } } + + public List getTransactionsFromSignature(byte[] signature) throws DataException { + List transactions = new ArrayList(); + + try { + ResultSet rs = DB.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature); + if (rs == null) + return transactions; // No transactions in this block + + TransactionRepository transactionRepo = RepositoryManager.getTransactionRepository(); + + // NB: do-while loop because DB.checkedExecute() implicitly calls ResultSet.next() for us + do { + byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1)); + transactions.add(transactionRepo.fromSignature(transactionSignature)); + } while (rs.next()); + } catch (SQLException e) { + throw new DataException(e); + } + + return transactions; + } + + public void save(BlockData blockData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks"); + + saveHelper.bind("signature", blockData.getSignature()).bind("version", blockData.getVersion()).bind("reference", blockData.getReference()) + .bind("transaction_count", blockData.getTransactionCount()).bind("total_fees", blockData.getTotalFees()).bind("transactions_signature", blockData.getTransactionsSignature()) + .bind("height", blockData.getHeight()).bind("generation", new Timestamp(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance()) + .bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature()).bind("AT_data", blockData.getAtBytes()) + .bind("AT_fees", blockData.getAtFees()); + + try { + saveHelper.execute(); + } catch (SQLException e) { + throw new DataException("Unable to save Block into repository", e); + } + } + } diff --git a/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java b/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java index 605f3495..6b10e92f 100644 --- a/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java +++ b/src/repository/hsqldb/HSQLDBGenesisTransactionRepository.java @@ -4,11 +4,10 @@ import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; -import data.account.Account; -import data.account.PublicKeyAccount; -import data.transaction.GenesisTransaction; -import data.transaction.Transaction; +import data.transaction.GenesisTransactionData; +import data.transaction.TransactionData; import database.DB; +import repository.DataException; public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionRepository { @@ -16,19 +15,34 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit super(repository); } - Transaction fromBase(byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) { + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) { try { ResultSet rs = DB.checkedExecute(repository.connection, "SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); if (rs == null) return null; - Account recipient = new Account(rs.getString(1)); + String recipient = rs.getString(1); BigDecimal amount = rs.getBigDecimal(2).setScale(8); - return new GenesisTransaction(recipient, amount, timestamp, signature); + return new GenesisTransactionData(recipient, amount, timestamp, signature); } catch (SQLException e) { return null; } } + @Override + public void save(TransactionData transaction) throws DataException { + super.save(transaction); + + GenesisTransactionData genesisTransaction = (GenesisTransactionData) transaction; + + HSQLDBSaver saveHelper = new HSQLDBSaver("GenesisTransactions"); + saveHelper.bind("signature", genesisTransaction.getSignature()).bind("recipient", genesisTransaction.getRecipient()).bind("amount", genesisTransaction.getAmount()); + try { + saveHelper.execute(); + } catch (SQLException e) { + throw new DataException(e); + } + } + } diff --git a/src/repository/hsqldb/HSQLDBTransactionRepository.java b/src/repository/hsqldb/HSQLDBTransactionRepository.java index baded758..403d98af 100644 --- a/src/repository/hsqldb/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/HSQLDBTransactionRepository.java @@ -6,11 +6,12 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; -import data.account.PublicKeyAccount; -import data.transaction.Transaction; -import data.transaction.Transaction.TransactionType; +import data.block.BlockData; +import data.transaction.TransactionData; +import qora.transaction.Transaction.TransactionType; import database.DB; -import qora.block.Block; +import repository.DataException; +import repository.RepositoryManager; import repository.TransactionRepository; public class HSQLDBTransactionRepository implements TransactionRepository { @@ -23,7 +24,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository); } - public Transaction fromSignature(byte[] signature) { + public TransactionData fromSignature(byte[] signature) { try { ResultSet rs = DB.checkedExecute(repository.connection, "SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); if (rs == null) @@ -31,7 +32,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { TransactionType type = TransactionType.valueOf(rs.getInt(1)); byte[] reference = DB.getResultSetBytes(rs.getBinaryStream(2)); - PublicKeyAccount creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(3))); + byte[] creator = DB.getResultSetBytes(rs.getBinaryStream(3)); long timestamp = rs.getTimestamp(4).getTime(); BigDecimal fee = rs.getBigDecimal(5).setScale(8); @@ -41,7 +42,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } - public Transaction fromReference(byte[] reference) { + public TransactionData fromReference(byte[] reference) { try { ResultSet rs = DB.checkedExecute(repository.connection, "SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference); if (rs == null) @@ -49,7 +50,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { TransactionType type = TransactionType.valueOf(rs.getInt(1)); byte[] signature = DB.getResultSetBytes(rs.getBinaryStream(2)); - PublicKeyAccount creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(3))); + byte[] creator = DB.getResultSetBytes(rs.getBinaryStream(3)); long timestamp = rs.getTimestamp(4).getTime(); BigDecimal fee = rs.getBigDecimal(5).setScale(8); @@ -59,7 +60,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } - private Transaction fromBase(TransactionType type, byte[] signature, byte[] reference, PublicKeyAccount creator, long timestamp, BigDecimal fee) { + private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creator, long timestamp, BigDecimal fee) { switch (type) { case GENESIS: return this.genesisTransactionRepository.fromBase(signature, reference, creator, timestamp, fee); @@ -70,26 +71,27 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } @Override - public int getHeight(Transaction transaction) { - byte[] signature = transaction.getSignature(); + public int getHeight(TransactionData transactionData) { + byte[] signature = transactionData.getSignature(); if (signature == null) return 0; // in one go? try { ResultSet rs = DB.checkedExecute(repository.connection, "SELECT height from BlockTransactions JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature WHERE transaction_signature = ? LIMIT 1", signature); + if (rs == null) return 0; - + return rs.getInt(1); } catch (SQLException e) { return 0; } } - + @Override - public Block toBlock(Transaction transaction) { - byte[] signature = transaction.getSignature(); + public BlockData toBlock(TransactionData transactionData) { + byte[] signature = transactionData.getSignature(); if (signature == null) return null; @@ -98,33 +100,30 @@ public class HSQLDBTransactionRepository implements TransactionRepository { ResultSet rs = DB.checkedExecute(repository.connection, "SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1", signature); if (rs == null) return null; - + byte[] blockSignature = DB.getResultSetBytes(rs.getBinaryStream(1)); - - // TODO - // return RepositoryManager.getBlockRepository().fromSignature(blockSignature); - - return null; - } catch (SQLException e) { + + return RepositoryManager.getBlockRepository().fromSignature(blockSignature); + } catch (SQLException | DataException e) { return null; } } @Override - public void save(Transaction transaction) { + public void save(TransactionData transactionData) throws DataException { HSQLDBSaver saver = new HSQLDBSaver("Transactions"); - saver.bind("signature", transaction.getSignature()).bind("reference", transaction.getReference()).bind("type", transaction.getType().value) - .bind("creator", transaction.getCreator().getPublicKey()).bind("creation", new Timestamp(transaction.getTimestamp())).bind("fee", transaction.getFee()) + saver.bind("signature", transactionData.getSignature()).bind("reference", transactionData.getReference()).bind("type", transactionData.getType().value) + .bind("creator", transactionData.getCreatorPublicKey()).bind("creation", new Timestamp(transactionData.getTimestamp())).bind("fee", transactionData.getFee()) .bind("milestone_block", null); try { saver.execute(repository.connection); } catch (SQLException e) { - // XXX do what? + throw new DataException(e); } } @Override - public void delete(Transaction transaction) { + public void delete(TransactionData transactionData) { // NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY // definition. try { diff --git a/src/transform/Transformer.java b/src/transform/Transformer.java index e9dd7ec9..d35e8b7f 100644 --- a/src/transform/Transformer.java +++ b/src/transform/Transformer.java @@ -8,6 +8,7 @@ public abstract class Transformer { // Raw, not Base58-encoded public static final int ADDRESS_LENGTH = 25; + public static final int PUBLIC_KEY_LENGTH = 32; public static final int SIGNATURE_LENGTH = 64; public static final int TIMESTAMP_LENGTH = LONG_LENGTH; diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java new file mode 100644 index 00000000..2781839b --- /dev/null +++ b/src/transform/block/BlockTransformer.java @@ -0,0 +1,284 @@ +package transform.block; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.block.BlockData; +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import qora.block.Block; +import qora.transaction.Transaction; +import repository.DataException; +import transform.TransformationException; +import transform.Transformer; +import transform.transaction.TransactionTransformer; +import utils.Base58; +import utils.Serialization; + +public class BlockTransformer extends Transformer { + + private static final int VERSION_LENGTH = INT_LENGTH; + private static final int TRANSACTIONS_SIGNATURE_LENGTH = SIGNATURE_LENGTH; + private static final int GENERATOR_SIGNATURE_LENGTH = SIGNATURE_LENGTH; + private static final int BLOCK_REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; + private static final int TIMESTAMP_LENGTH = LONG_LENGTH; + private static final int GENERATING_BALANCE_LENGTH = LONG_LENGTH; + private static final int GENERATOR_LENGTH = PUBLIC_KEY_LENGTH; + private static final int TRANSACTION_COUNT_LENGTH = INT_LENGTH; + + private static final int BASE_LENGTH = VERSION_LENGTH + BLOCK_REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH + + TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH; + + protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; + protected static final int TRANSACTION_SIZE_LENGTH = INT_LENGTH; // per transaction + protected static final int AT_BYTES_LENGTH = INT_LENGTH; + protected static final int AT_FEES_LENGTH = LONG_LENGTH; + protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; + + public static BlockData fromBytes(byte[] bytes) throws TransformationException { + if (bytes == null) + return null; + + if (bytes.length < BASE_LENGTH) + throw new TransformationException("Byte data too short for Block"); + + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + + int version = byteBuffer.getInt(); + + if (version >= 2 && bytes.length < BASE_LENGTH + AT_LENGTH) + throw new TransformationException("Byte data too short for V2+ Block"); + + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[BLOCK_REFERENCE_LENGTH]; + byteBuffer.get(reference); + + BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8); + byte[] generatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH]; + byteBuffer.get(transactionsSignature); + byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH]; + byteBuffer.get(generatorSignature); + + byte[] atBytes = null; + BigDecimal atFees = null; + if (version >= 2) { + int atBytesLength = byteBuffer.getInt(); + + if (atBytesLength > Block.MAX_BLOCK_BYTES) + throw new TransformationException("Byte data too long for Block's AT info"); + + atBytes = new byte[atBytesLength]; + byteBuffer.get(atBytes); + + atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8); + } + + int transactionCount = byteBuffer.getInt(); + + // Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be + List transactions = new ArrayList(); + for (int t = 0; t < transactionCount; ++t) { + if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH) + throw new TransformationException("Byte data too short for Block Transaction length"); + + int transactionLength = byteBuffer.getInt(); + if (byteBuffer.remaining() < transactionLength) + throw new TransformationException("Byte data too short for Block Transaction"); + if (transactionLength > Block.MAX_BLOCK_BYTES) + throw new TransformationException("Byte data too long for Block Transaction"); + + byte[] transactionBytes = new byte[transactionLength]; + byteBuffer.get(transactionBytes); + + TransactionData transaction = TransactionTransformer.fromBytes(transactionBytes); + transactions.add(transaction); + } + + if (byteBuffer.hasRemaining()) + throw new TransformationException("Excess byte data found after parsing Block"); + + // XXX Can't return a simple BlockData object because it doesn't support holding the transactions + // return new BlockData(version, reference, timestamp, generatingBalance, generatorPublicKey, generatorSignature, transactionsSignature, atBytes, atFees, transactions); + return null; + } + + public static int getDataLength(BlockData blockData) throws TransformationException { + // TODO + int blockLength = BASE_LENGTH; + + if (blockData.getVersion() >= 2 && blockData.getAtBytes() != null) + blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getAtBytes().length; + + /* + * XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData? + // Short cut for no transactions + if (block.getTransactions() == null || block.getTransactions().isEmpty()) + return blockLength; + + for (TransactionData transaction : this.transactions) + blockLength += TRANSACTION_SIZE_LENGTH + transaction.getDataLength(); + */ + + return blockLength; + } + + public static byte[] toBytes(BlockData blockData) throws TransformationException { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength(blockData)); + + bytes.write(Ints.toByteArray(blockData.getVersion())); + bytes.write(Longs.toByteArray(blockData.getTimestamp())); + bytes.write(blockData.getReference()); + // NOTE: generatingBalance serialized as long value, not as BigDecimal, for historic compatibility + bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue())); + bytes.write(blockData.getGeneratorPublicKey()); + bytes.write(blockData.getTransactionsSignature()); + bytes.write(blockData.getGeneratorSignature()); + + if (blockData.getVersion() >= 2) { + byte[] atBytes = blockData.getAtBytes(); + + if (atBytes != null) { + bytes.write(Ints.toByteArray(atBytes.length)); + bytes.write(atBytes); + // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility + bytes.write(Longs.toByteArray(blockData.getAtFees().longValue())); + } else { + bytes.write(Ints.toByteArray(0)); + bytes.write(Longs.toByteArray(0L)); + } + } + + // Transactions + bytes.write(Ints.toByteArray(blockData.getTransactionCount())); + + /* + * XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData? + for (TransactionData transaction : blockData.getTransactions()) { + bytes.write(Ints.toByteArray(transaction.getDataLength())); + bytes.write(transaction.toBytes()); + } + */ + + return bytes.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(BlockData blockData) throws TransformationException { + JSONObject json = new JSONObject(); + + json.put("version", blockData.getVersion()); + json.put("timestamp", blockData.getTimestamp()); + json.put("generatingBalance", blockData.getGeneratingBalance()); + json.put("generator", new PublicKeyAccount(blockData.getGeneratorPublicKey()).getAddress()); + json.put("generatorPublicKey", Base58.encode(blockData.getGeneratorPublicKey())); + json.put("fee", blockData.getTotalFees().toPlainString()); + json.put("transactionsSignature", Base58.encode(blockData.getTransactionsSignature())); + json.put("generatorSignature", Base58.encode(blockData.getGeneratorSignature())); + json.put("signature", Base58.encode(blockData.getSignature())); + + if (blockData.getReference() != null) + json.put("reference", Base58.encode(blockData.getReference())); + + json.put("height", blockData.getHeight()); + + // Add transaction info + JSONArray transactionsJson = new JSONArray(); + boolean tradesHappened = false; + + /* + * XXX Where do the transactions come from? A param? Do we pass a Block instead of BlockData? + for (TransactionData transaction : blockData.getTransactions()) { + transactionsJson.add(transaction.toJSON()); + + // If this is an asset CreateOrderTransaction then check to see if any trades happened + if (transaction.getType() == Transaction.TransactionType.CREATE_ASSET_ORDER) { + CreateOrderTransaction orderTransaction = (CreateOrderTransaction) transaction; + Order order = orderTransaction.getOrder(); + List trades = order.getTrades(); + + // Filter out trades with timestamps that don't match order transaction's timestamp + trades.removeIf((Trade trade) -> trade.getTimestamp() != order.getTimestamp()); + + // Any trades left? + if (!trades.isEmpty()) { + tradesHappened = true; + + // No need to check any further + break; + } + } + } + json.put("transactions", transactionsJson); + */ + + // Add asset trade activity flag + json.put("assetTrades", tradesHappened); + + // Add CIYAM AT info (if any) + if (blockData.getAtBytes() != null) { + json.put("blockATs", HashCode.fromBytes(blockData.getAtBytes()).toString()); + json.put("atFees", blockData.getAtFees()); + } + + return json; + } + + public static byte[] getBytesForGeneratorSignature(BlockData blockData) throws TransformationException { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); + + // Only copy the generator signature from reference, which is the first 64 bytes. + bytes.write(Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH)); + + bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().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(blockData.getGeneratorPublicKey(), 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); + + try { + bytes.write(block.getBlockData().getGeneratorSignature()); + + for (Transaction transaction : block.getTransactions()) { + if (!transaction.isSignatureValid()) + return null; + + bytes.write(transaction.getTransactionData().getSignature()); + } + + return bytes.toByteArray(); + } catch (IOException | DataException e) { + throw new TransformationException(e); + } + } + +} diff --git a/src/transform/transaction/GenesisTransactionTransformer.java b/src/transform/transaction/GenesisTransactionTransformer.java index dab34ba8..0baf4ece 100644 --- a/src/transform/transaction/GenesisTransactionTransformer.java +++ b/src/transform/transaction/GenesisTransactionTransformer.java @@ -10,9 +10,8 @@ import org.json.simple.JSONObject; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; -import data.transaction.Transaction; -import data.account.Account; -import data.transaction.GenesisTransaction; +import data.transaction.TransactionData; +import data.transaction.GenesisTransactionData; import transform.TransformationException; import utils.Base58; import utils.Serialization; @@ -25,30 +24,30 @@ public class GenesisTransactionTransformer extends TransactionTransformer { // Note that Genesis transactions don't require reference, fee or signature: private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; - static Transaction fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { if (byteBuffer.remaining() < TYPELESS_LENGTH) throw new TransformationException("Byte data too short for GenesisTransaction"); long timestamp = byteBuffer.getLong(); - Account recipient = new Account(Serialization.deserializeRecipient(byteBuffer)); + String recipient = Serialization.deserializeRecipient(byteBuffer); BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); - return new GenesisTransaction(recipient, amount, timestamp); + return new GenesisTransactionData(recipient, amount, timestamp); } - public static int getDataLength(Transaction baseTransaction) throws TransformationException { + public static int getDataLength(TransactionData baseTransaction) throws TransformationException { return TYPE_LENGTH + TYPELESS_LENGTH; } - public static byte[] toBytes(Transaction baseTransaction) throws TransformationException { + public static byte[] toBytes(TransactionData baseTransaction) throws TransformationException { try { - GenesisTransaction transaction = (GenesisTransaction) baseTransaction; + GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(Ints.toByteArray(transaction.getType().value)); bytes.write(Longs.toByteArray(transaction.getTimestamp())); - bytes.write(Base58.decode(transaction.getRecipient().getAddress())); + bytes.write(Base58.decode(transaction.getRecipient())); bytes.write(Serialization.serializeBigDecimal(transaction.getAmount())); return bytes.toByteArray(); @@ -58,13 +57,13 @@ public class GenesisTransactionTransformer extends TransactionTransformer { } @SuppressWarnings("unchecked") - public static JSONObject toJSON(Transaction baseTransaction) throws TransformationException { + public static JSONObject toJSON(TransactionData baseTransaction) throws TransformationException { JSONObject json = TransactionTransformer.getBaseJSON(baseTransaction); try { - GenesisTransaction transaction = (GenesisTransaction) baseTransaction; + GenesisTransactionData transaction = (GenesisTransactionData) baseTransaction; - json.put("recipient", transaction.getRecipient().getAddress()); + json.put("recipient", transaction.getRecipient()); json.put("amount", transaction.getAmount().toPlainString()); } catch (ClassCastException e) { throw new TransformationException(e); diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index b5d6223f..e118ab66 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -4,8 +4,8 @@ import java.nio.ByteBuffer; import org.json.simple.JSONObject; -import data.transaction.Transaction; -import data.transaction.Transaction.TransactionType; +import data.transaction.TransactionData; +import qora.transaction.Transaction.TransactionType; import transform.TransformationException; import transform.Transformer; import utils.Base58; @@ -14,7 +14,7 @@ public class TransactionTransformer extends Transformer { protected static final int TYPE_LENGTH = INT_LENGTH; - public static Transaction fromBytes(byte[] bytes) throws TransformationException { + public static TransactionData fromBytes(byte[] bytes) throws TransformationException { if (bytes == null) return null; @@ -36,7 +36,7 @@ public class TransactionTransformer extends Transformer { } } - public static int getDataLength(Transaction transaction) throws TransformationException { + public static int getDataLength(TransactionData transaction) throws TransformationException { switch (transaction.getType()) { case GENESIS: return GenesisTransactionTransformer.getDataLength(transaction); @@ -46,7 +46,7 @@ public class TransactionTransformer extends Transformer { } } - public static byte[] toBytes(Transaction transaction) throws TransformationException { + public static byte[] toBytes(TransactionData transaction) throws TransformationException { switch (transaction.getType()) { case GENESIS: return GenesisTransactionTransformer.toBytes(transaction); @@ -56,7 +56,7 @@ public class TransactionTransformer extends Transformer { } } - public static JSONObject toJSON(Transaction transaction) throws TransformationException { + public static JSONObject toJSON(TransactionData transaction) throws TransformationException { switch (transaction.getType()) { case GENESIS: return GenesisTransactionTransformer.toJSON(transaction); @@ -67,7 +67,7 @@ public class TransactionTransformer extends Transformer { } @SuppressWarnings("unchecked") - static JSONObject getBaseJSON(Transaction transaction) { + static JSONObject getBaseJSON(TransactionData transaction) { JSONObject json = new JSONObject(); json.put("type", transaction.getType().value); diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 37caa590..551461ac 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -6,8 +6,8 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import qora.account.PublicKeyAccount; -import qora.transaction.TransactionHandler; import transform.TransformationException; +import transform.Transformer; public class Serialization { @@ -31,15 +31,15 @@ public class Serialization { } public static String deserializeRecipient(ByteBuffer byteBuffer) { - byte[] bytes = new byte[TransactionHandler.RECIPIENT_LENGTH]; + byte[] bytes = new byte[Transformer.ADDRESS_LENGTH]; byteBuffer.get(bytes); return Base58.encode(bytes); } - public static PublicKeyAccount deserializePublicKey(ByteBuffer byteBuffer) { - byte[] bytes = new byte[TransactionHandler.CREATOR_LENGTH]; + public static byte[] deserializePublicKey(ByteBuffer byteBuffer) { + byte[] bytes = new byte[Transformer.PUBLIC_KEY_LENGTH]; byteBuffer.get(bytes); - return new PublicKeyAccount(bytes); + return bytes; } public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException {