diff --git a/src/database/DB.java b/src/database/DB.java index ca2ffdf7..6c5ba0fd 100644 --- a/src/database/DB.java +++ b/src/database/DB.java @@ -10,6 +10,8 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; +import org.hsqldb.jdbc.JDBCPool; + import com.google.common.primitives.Bytes; /** @@ -18,6 +20,33 @@ import com.google.common.primitives.Bytes; */ public class DB { + private static JDBCPool connectionPool; + private static String connectionUrl; + + public static void open() throws SQLException { + connectionPool = new JDBCPool(); + connectionPool.setUrl(connectionUrl); + } + + public static void setUrl(String url) { + connectionUrl = url; + } + + /** + * Return an on-demand Connection from connection pool. + *

+ * Mostly used in database-read scenarios whereas database-write scenarios, especially multi-statement transactions, are likely to pass around a Connection + * object. + *

+ * By default HSQLDB will wait up to 30 seconds for a pooled connection to become free. + * + * @return Connection + * @throws SQLException + */ + public static Connection getConnection() throws SQLException { + return connectionPool.getConnection(); + } + public static void startTransaction(Connection c) throws SQLException { c.prepareStatement("START TRANSACTION").execute(); } @@ -30,6 +59,11 @@ public class DB { c.prepareStatement("ROLLBACK").execute(); } + public static void close() throws SQLException { + getConnection().createStatement().execute("SHUTDOWN"); + connectionPool.close(0); + } + /** * Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of set length. * @@ -71,6 +105,9 @@ public class DB { try { byte[] buffer = new byte[BYTE_BUFFER_LENGTH]; int length = inputStream.read(buffer); + if (length == -1) + break; + result = Bytes.concat(result, Arrays.copyOf(buffer, length)); } catch (IOException e) { // No more bytes @@ -117,6 +154,8 @@ public class DB { * Note that each object is bound to two place-holders based on this SQL syntax: *

* INSERT INTO table (column, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE column=?, ... + *

+ * Requires that mySQL SQL syntax support is enabled during connection. * * @param preparedStatement * @param objects @@ -145,27 +184,28 @@ public class DB { *

* Typically used to fetch Blocks or Transactions using signature or reference. * - * @param connection * @param sql * @param bytes * @return ResultSet, or null if no matching rows found * @throws SQLException */ - public static ResultSet executeUsingBytes(Connection connection, String sql, byte[] bytes) throws SQLException { - PreparedStatement preparedStatement = connection.prepareStatement(sql); - preparedStatement.setBinaryStream(1, new ByteArrayInputStream(bytes)); + public static ResultSet executeUsingBytes(String sql, byte[] bytes) throws SQLException { + try (final Connection connection = DB.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement(sql); + preparedStatement.setBinaryStream(1, new ByteArrayInputStream(bytes)); - if (!preparedStatement.execute()) - throw new SQLException("Fetching from database produced no results"); + if (!preparedStatement.execute()) + throw new SQLException("Fetching from database produced no results"); - ResultSet rs = preparedStatement.getResultSet(); - if (rs == null) - throw new SQLException("Fetching results from database produced no ResultSet"); + ResultSet rs = preparedStatement.getResultSet(); + if (rs == null) + throw new SQLException("Fetching results from database produced no ResultSet"); - if (!rs.next()) - return null; + if (!rs.next()) + return null; - return rs; + return rs; + } } /** diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 3230f63f..1877fc6e 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -1,16 +1,20 @@ package qora.block; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.json.simple.JSONObject; import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; import database.DB; import database.NoDataFoundException; @@ -89,6 +93,9 @@ public class Block { protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; // Constructors + + // For creating a new block from scratch or instantiating one that was previously serialized + // XXX shouldn't transactionsSignature be passed in here? protected Block(int version, byte[] reference, long timestamp, BigDecimal generationTarget, PublicKeyAccount generator, byte[] generationSignature, byte[] atBytes, BigDecimal atFees) { this.version = version; @@ -100,7 +107,7 @@ public class Block { this.height = 0; this.transactionCount = 0; - this.transactions = null; + this.transactions = new ArrayList(); this.transactionsSignature = null; this.totalFees = null; @@ -183,20 +190,15 @@ public class Block { return blockLength; } - public List getTransactions() { - return this.transactions; - } - /** - * Return block's transactions from DB (or cache). + * Return block's transactions. *

- * If block's transactions have already been loaded from DB then the cached copied is returned instead. + * 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. * - * @param connection * @return * @throws SQLException */ - public List getTransactions(Connection connection) throws SQLException { + public List getTransactions() throws SQLException { // Already loaded? if (this.transactions != null) return this.transactions; @@ -204,16 +206,14 @@ public class Block { // Allocate cache for results this.transactions = new ArrayList(); - // Load from DB - ResultSet rs = DB.executeUsingBytes(connection, "SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature()); + ResultSet rs = DB.executeUsingBytes("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", this.getSignature()); if (rs == null) return this.transactions; // No transactions in this block - // Use each row's signature to load, and cache, Transactions // NB: do-while loop because DB.executeUsingBytes() implicitly calls ResultSet.next() for us do { byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); - this.transactions.add(TransactionFactory.fromSignature(connection, transactionSignature)); + this.transactions.add(TransactionFactory.fromSignature(transactionSignature)); } while (rs.next()); return this.transactions; @@ -221,8 +221,8 @@ public class Block { // Load/Save - protected Block(Connection connection, byte[] signature) throws SQLException { - this(DB.executeUsingBytes(connection, "SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature)); + protected Block(byte[] signature) throws SQLException { + this(DB.executeUsingBytes("SELECT " + DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature)); } protected Block(ResultSet rs) throws SQLException { @@ -246,14 +246,13 @@ public class Block { /** * Load Block from DB using block signature. * - * @param connection * @param signature * @return Block, or null if not found * @throws SQLException */ - public static Block fromSignature(Connection connection, byte[] signature) throws SQLException { + public static Block fromSignature(byte[] signature) throws SQLException { try { - return new Block(connection, signature); + return new Block(signature); } catch (NoDataFoundException e) { return null; } @@ -262,19 +261,20 @@ public class Block { /** * Load Block from DB using block height * - * @param connection * @param height * @return Block, or null if not found * @throws SQLException */ - public static Block fromHeight(Connection connection, int height) throws SQLException { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT signature FROM Blocks WHERE height = ?"); - preparedStatement.setInt(1, height); + public static Block fromHeight(int height) throws SQLException { + try (final Connection connection = DB.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement("SELECT " + DB_COLUMNS + " FROM Blocks WHERE height = ?"); + preparedStatement.setInt(1, height); - try { - return new Block(DB.checkedExecute(preparedStatement)); - } catch (NoDataFoundException e) { - return null; + try { + return new Block(DB.checkedExecute(preparedStatement)); + } catch (NoDataFoundException e) { + return null; + } } } @@ -295,13 +295,12 @@ public class Block { /** * Load parent Block from DB * - * @param connection * @return Block, or null if not found * @throws SQLException */ - public Block getParent(Connection connection) throws SQLException { + public Block getParent() throws SQLException { try { - return new Block(connection, this.reference); + return new Block(this.reference); } catch (NoDataFoundException e) { return null; } @@ -310,16 +309,15 @@ public class Block { /** * Load child Block from DB * - * @param connection * @return Block, or null if not found * @throws SQLException */ - public Block getChild(Connection connection) throws SQLException { + public Block getChild() throws SQLException { byte[] blockSignature = this.getSignature(); if (blockSignature == null) return null; - ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature); + ResultSet resultSet = DB.executeUsingBytes("SELECT " + DB_COLUMNS + " FROM Blocks WHERE reference = ?", blockSignature); try { return new Block(resultSet); @@ -356,9 +354,44 @@ public class Block { return null; } - public boolean isSignatureValid(PublicKeyAccount signer) { - // TODO - return false; + private byte[] getBytesForSignature() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(REFERENCE_LENGTH + GENERATION_TARGET_LENGTH + GENERATOR_LENGTH); + // Only copy the generator signature from reference, which is the first 64 bytes. + bytes.write(Arrays.copyOf(this.reference, GENERATION_SIGNATURE_LENGTH)); + bytes.write(Longs.toByteArray(this.generationTarget.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); + } + } + + public boolean isSignatureValid() { + // Check generator's signature first + if (!this.generator.verify(this.generationSignature, getBytesForSignature())) + return false; + + // Check transactions signature + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATION_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH); + try { + bytes.write(this.generationSignature); + + for (Transaction transaction : this.getTransactions()) { + if (!transaction.isSignatureValid()) + return false; + + bytes.write(transaction.getSignature()); + } + } catch (IOException | SQLException e) { + throw new RuntimeException(e); + } + + if (!this.generator.verify(this.transactionsSignature, bytes.toByteArray())) + return false; + + return true; } public boolean isValid(Connection connection) throws SQLException { diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index d0810409..3a1cc3b6 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -15,13 +15,12 @@ public class BlockChain { /** * Return block height from DB using signature. * - * @param connection * @param signature * @return height, or 0 if block not found. * @throws SQLException */ - public static int getBlockHeightFromSignature(Connection connection, byte[] signature) throws SQLException { - ResultSet rs = DB.executeUsingBytes(connection, "SELECT height FROM Blocks WHERE signature = ?", signature); + public static int getBlockHeightFromSignature(byte[] signature) throws SQLException { + ResultSet rs = DB.executeUsingBytes("SELECT height FROM Blocks WHERE signature = ?", signature); if (rs == null) return 0; @@ -31,16 +30,17 @@ public class BlockChain { /** * Return highest block height from DB. * - * @param connection * @return height, or 0 if there are no blocks in DB (not very likely). * @throws SQLException */ - public static int getMaxHeight(Connection connection) throws SQLException { - ResultSet rs = DB.checkedExecute(connection.prepareStatement("SELECT MAX(height) FROM Blocks")); - if (rs == null) - return 0; + public static int getMaxHeight() throws SQLException { + try (final Connection connection = DB.getConnection()) { + ResultSet rs = DB.checkedExecute(connection.prepareStatement("SELECT MAX(height) FROM Blocks")); + if (rs == null) + return 0; - return rs.getInt(1); + return rs.getInt(1); + } } } diff --git a/src/qora/block/BlockFactory.java b/src/qora/block/BlockFactory.java index 07f37c44..69716a5a 100644 --- a/src/qora/block/BlockFactory.java +++ b/src/qora/block/BlockFactory.java @@ -12,13 +12,12 @@ public class BlockFactory { /** * Load Block from DB using block signature. * - * @param connection * @param signature * @return ? extends Block, or null if not found * @throws SQLException */ - public static Block fromSignature(Connection connection, byte[] signature) throws SQLException { - Block block = Block.fromSignature(connection, signature); + public static Block fromSignature(byte[] signature) throws SQLException { + Block block = Block.fromSignature(signature); if (block == null) return null; @@ -33,22 +32,23 @@ public class BlockFactory { /** * Load Block from DB using block height * - * @param connection * @param height * @return ? extends Block, or null if not found * @throws SQLException */ - public static Block fromHeight(Connection connection, int height) throws SQLException { + public static Block fromHeight(int height) throws SQLException { if (height == 1) return GenesisBlock.getInstance(); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT signature FROM Blocks WHERE height = ?"); - preparedStatement.setInt(1, height); + try (final Connection connection = DB.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement("SELECT signature FROM Blocks WHERE height = ?"); + preparedStatement.setInt(1, height); - try { - return new Block(DB.checkedExecute(preparedStatement)); - } catch (NoDataFoundException e) { - return null; + try { + return new Block(DB.checkedExecute(preparedStatement)); + } catch (NoDataFoundException e) { + return null; + } } } diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index f632ceab..f8a4f613 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -47,25 +47,26 @@ public class BlockTransaction { // Load/Save - protected BlockTransaction(Connection connection, byte[] blockSignature, int sequence) throws SQLException { - // Can't use DB.executeUsingBytes() here as we need two placeholders - PreparedStatement preparedStatement = connection - .prepareStatement("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? and sequence = ?"); - preparedStatement.setBinaryStream(1, new ByteArrayInputStream(blockSignature)); - preparedStatement.setInt(2, sequence); + protected BlockTransaction(byte[] blockSignature, int sequence) throws SQLException { + try (final Connection connection = DB.getConnection()) { + // Can't use DB.executeUsingBytes() here as we need two placeholders + PreparedStatement preparedStatement = connection + .prepareStatement("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? and sequence = ?"); + preparedStatement.setBinaryStream(1, new ByteArrayInputStream(blockSignature)); + preparedStatement.setInt(2, sequence); - ResultSet rs = DB.checkedExecute(preparedStatement); - if (rs == null) - throw new NoDataFoundException(); + ResultSet rs = DB.checkedExecute(preparedStatement); + if (rs == null) + throw new NoDataFoundException(); - this.blockSignature = blockSignature; - this.sequence = sequence; - this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + this.blockSignature = blockSignature; + this.sequence = sequence; + this.transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); + } } - protected BlockTransaction(Connection connection, byte[] transactionSignature) throws SQLException { - ResultSet rs = DB.executeUsingBytes(connection, "SELECT block_signature, sequence FROM BlockTransactions WHERE transaction_signature = ?", - transactionSignature); + protected BlockTransaction(byte[] transactionSignature) throws SQLException { + ResultSet rs = DB.executeUsingBytes("SELECT block_signature, sequence FROM BlockTransactions WHERE transaction_signature = ?", transactionSignature); if (rs == null) throw new NoDataFoundException(); @@ -77,15 +78,14 @@ public class BlockTransaction { /** * Load BlockTransaction from DB using block signature and tx-in-block sequence. * - * @param connection * @param blockSignature * @param sequence * @return BlockTransaction, or null if not found * @throws SQLException */ - public static BlockTransaction fromBlockSignature(Connection connection, byte[] blockSignature, int sequence) throws SQLException { + public static BlockTransaction fromBlockSignature(byte[] blockSignature, int sequence) throws SQLException { try { - return new BlockTransaction(connection, blockSignature, sequence); + return new BlockTransaction(blockSignature, sequence); } catch (NoDataFoundException e) { return null; } @@ -94,14 +94,13 @@ public class BlockTransaction { /** * Load BlockTransaction from DB using transaction signature. * - * @param connection * @param transactionSignature * @return BlockTransaction, or null if not found * @throws SQLException */ - public static BlockTransaction fromTransactionSignature(Connection connection, byte[] transactionSignature) throws SQLException { + public static BlockTransaction fromTransactionSignature(byte[] transactionSignature) throws SQLException { try { - return new BlockTransaction(connection, transactionSignature); + return new BlockTransaction(transactionSignature); } catch (NoDataFoundException e) { return null; } @@ -119,23 +118,21 @@ public class BlockTransaction { /** * Load corresponding Block from DB. * - * @param connection * @return Block, or null if not found (which should never happen) * @throws SQLException */ - public Block getBlock(Connection connection) throws SQLException { - return Block.fromSignature(connection, this.blockSignature); + public Block getBlock() throws SQLException { + return Block.fromSignature(this.blockSignature); } /** * Load corresponding Transaction from DB. * - * @param connection * @return Transaction, or null if not found (which should never happen) * @throws SQLException */ - public Transaction getTransaction(Connection connection) throws SQLException { - return TransactionFactory.fromSignature(connection, this.transactionSignature); + public Transaction getTransaction() throws SQLException { + return TransactionFactory.fromSignature(this.transactionSignature); } // Converters diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 2a736935..07aa4c27 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -14,7 +14,6 @@ import com.google.common.primitives.Longs; import qora.account.GenesisAccount; import qora.account.PrivateKeyAccount; -import qora.account.PublicKeyAccount; import qora.crypto.Crypto; import qora.transaction.GenesisTransaction; import qora.transaction.Transaction; @@ -33,10 +32,11 @@ public class GenesisBlock extends Block { // Constructors protected GenesisBlock() { - super(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, GENESIS_TIMESTAMP, GENESIS_GENERATION_TARGET, GENESIS_GENERATOR, GENESIS_GENERATION_SIGNATURE, null, null); + super(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, GENESIS_TIMESTAMP, GENESIS_GENERATION_TARGET, GENESIS_GENERATOR, GENESIS_GENERATION_SIGNATURE, null, + null); this.height = 1; - + this.transactions = new ArrayList(); // Genesis transactions @@ -174,7 +174,7 @@ public class GenesisBlock extends Block { addGenesisTransaction("QgcphUTiVHHfHg8e1LVgg5jujVES7ZDUTr", "115031531"); addGenesisTransaction("QbQk9s4j4EAxAguBhmqA8mdtTct3qGnsrx", "138348733.2"); addGenesisTransaction("QT79PhvBwE6vFzfZ4oh5wdKVsEazZuVJFy", "6360421.343"); - + this.transactionsSignature = GENESIS_TRANSACTIONS_SIGNATURE; } @@ -197,7 +197,7 @@ public class GenesisBlock extends Block { public static boolean isGenesisBlock(Block block) { if (block.height != 1) return false; - + // Validate block signature if (!Arrays.equals(GENESIS_GENERATION_SIGNATURE, block.generationSignature)) return false; @@ -211,8 +211,8 @@ public class GenesisBlock extends Block { // Load/Save - protected GenesisBlock(Connection connection, byte[] signature) throws SQLException { - super(connection, signature); + protected GenesisBlock(byte[] signature) throws SQLException { + super(signature); } protected GenesisBlock(ResultSet rs) throws SQLException { @@ -226,12 +226,11 @@ public class GenesisBlock extends Block { *

* As the genesis block is the first block, this always returns null. * - * @param connection * @return null * @throws SQLException */ @Override - public Block getParent(Connection connection) throws SQLException { + public Block getParent() throws SQLException { return null; } @@ -248,7 +247,7 @@ public class GenesisBlock extends Block { private void addGenesisTransaction(String recipient, String amount) { this.transactions.add(new GenesisTransaction(recipient, new BigDecimal(amount).setScale(8), this.timestamp)); } - + /** * Refuse to calculate genesis block signature! *

@@ -279,7 +278,9 @@ public class GenesisBlock extends Block { private static byte[] getBytesForSignature() { try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(VERSION_LENGTH + REFERENCE_LENGTH + GENERATION_TARGET_LENGTH + GENERATOR_LENGTH); + // Passing expected size to ByteArrayOutputStream avoids reallocation when adding more bytes than default 32. + // See below for explanation of some of the values used to calculated expected size. + ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + GENERATION_TARGET_LENGTH + GENERATOR_LENGTH); /* * NOTE: Historic code had genesis block using Longs.toByteArray() compared to standard block's Ints.toByteArray. The subsequent * Bytes.ensureCapacity(versionBytes, 0, 4) did not truncate versionBytes back to 4 bytes either. This means 8 bytes were used even though @@ -292,6 +293,7 @@ public class GenesisBlock extends Block { */ bytes.write(Bytes.ensureCapacity(GENESIS_REFERENCE, 64, 0)); bytes.write(Longs.toByteArray(GENESIS_GENERATION_TARGET.longValue())); + // NOTE: Genesis account's public key is only 8 bytes, not the usual 32. bytes.write(GENESIS_GENERATOR.getPublicKey()); return bytes.toByteArray(); } catch (IOException e) { @@ -300,7 +302,7 @@ public class GenesisBlock extends Block { } @Override - public boolean isSignatureValid(PublicKeyAccount signer) { + public boolean isSignatureValid() { // Validate block signature if (!Arrays.equals(GENESIS_GENERATION_SIGNATURE, this.generationSignature)) return false; @@ -315,7 +317,7 @@ public class GenesisBlock extends Block { @Override public boolean isValid(Connection connection) throws SQLException { // Check there is no other block in DB - if (BlockChain.getMaxHeight(connection) != 0) + if (BlockChain.getMaxHeight() != 0) return false; // Validate transactions diff --git a/src/qora/crypto/Crypto.java b/src/qora/crypto/Crypto.java index 74558e96..678fac0b 100644 --- a/src/qora/crypto/Crypto.java +++ b/src/qora/crypto/Crypto.java @@ -80,9 +80,9 @@ public class Crypto { case ADDRESS_VERSION: case AT_ADDRESS_VERSION: byte[] addressWithoutChecksum = Arrays.copyOf(addressBytes, addressBytes.length - 4); - byte[] passedChecksum = Arrays.copyOfRange(addressWithoutChecksum, addressBytes.length - 4, addressBytes.length); + byte[] passedChecksum = Arrays.copyOfRange(addressBytes, addressBytes.length - 4, addressBytes.length); - byte[] generatedChecksum = doubleDigest(addressWithoutChecksum); + byte[] generatedChecksum = Arrays.copyOf(doubleDigest(addressWithoutChecksum), 4); return Arrays.equals(passedChecksum, generatedChecksum); default: diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index efbe8e39..6b573d2b 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -20,7 +20,6 @@ import database.NoDataFoundException; import qora.account.Account; import qora.account.GenesisAccount; import qora.account.PrivateKeyAccount; -import qora.account.PublicKeyAccount; import qora.crypto.Crypto; import utils.Base58; import utils.Serialization; @@ -40,7 +39,7 @@ public class GenesisTransaction extends Transaction { // Constructors public GenesisTransaction(String recipient, BigDecimal amount, long timestamp) { - super(TransactionType.PAYMENT, BigDecimal.ZERO, new GenesisAccount(), timestamp, new byte[0], null); + super(TransactionType.GENESIS, BigDecimal.ZERO, new GenesisAccount(), timestamp, null, null); this.recipient = new Account(recipient); this.amount = amount; @@ -68,34 +67,32 @@ public class GenesisTransaction extends Transaction { /** * Load GenesisTransaction from DB using signature. * - * @param connection * @param signature * @throws NoDataFoundException * if no matching row found * @throws SQLException */ - protected GenesisTransaction(Connection connection, byte[] signature) throws SQLException { - super(connection, TransactionType.GENESIS, signature); + protected GenesisTransaction(byte[] signature) throws SQLException { + super(TransactionType.GENESIS, signature); - ResultSet rs = DB.executeUsingBytes(connection, "SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); + ResultSet rs = DB.executeUsingBytes("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature); if (rs == null) throw new NoDataFoundException(); - this.recipient = new Account(rs.getString(2)); - this.amount = rs.getBigDecimal(3).setScale(8); + this.recipient = new Account(rs.getString(1)); + this.amount = rs.getBigDecimal(2).setScale(8); } /** * Load GenesisTransaction from DB using signature * - * @param connection * @param signature - * @return PaymentTransaction, or null if not found + * @return GenesisTransaction, or null if not found * @throws SQLException */ - public static GenesisTransaction fromSignature(Connection connection, byte[] signature) throws SQLException { + public static GenesisTransaction fromSignature(byte[] signature) throws SQLException { try { - return new GenesisTransaction(connection, signature); + return new GenesisTransaction(signature); } catch (NoDataFoundException e) { return null; } @@ -120,7 +117,7 @@ public class GenesisTransaction extends Transaction { @SuppressWarnings("unchecked") @Override - public JSONObject toJSON() { + public JSONObject toJSON() throws SQLException { JSONObject json = getBaseJSON(); json.put("recipient", this.recipient.getAddress()); @@ -173,7 +170,7 @@ public class GenesisTransaction extends Transaction { } /** - * Check validity of genesis transction signature. + * Check validity of genesis transaction signature. *

* This is handled differently as there is no private key for the genesis account and so no way to sign/verify data. *

@@ -182,7 +179,7 @@ public class GenesisTransaction extends Transaction { * @return boolean */ @Override - public boolean isSignatureValid(PublicKeyAccount signer) { + public boolean isSignatureValid() { return Arrays.equals(this.signature, calcSignature()); } diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 763e4bf7..b8144008 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -74,16 +74,15 @@ public class PaymentTransaction extends Transaction { /** * Load PaymentTransaction from DB using signature. * - * @param connection * @param signature * @throws NoDataFoundException * if no matching row found * @throws SQLException */ - protected PaymentTransaction(Connection connection, byte[] signature) throws SQLException { - super(connection, TransactionType.PAYMENT, signature); + protected PaymentTransaction(byte[] signature) throws SQLException { + super(TransactionType.PAYMENT, signature); - ResultSet rs = DB.executeUsingBytes(connection, "SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature); + ResultSet rs = DB.executeUsingBytes("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature); if (rs == null) throw new NoDataFoundException(); @@ -95,14 +94,13 @@ public class PaymentTransaction extends Transaction { /** * Load PaymentTransaction from DB using signature * - * @param connection * @param signature * @return PaymentTransaction, or null if not found * @throws SQLException */ - public static PaymentTransaction fromSignature(Connection connection, byte[] signature) throws SQLException { + public static PaymentTransaction fromSignature(byte[] signature) throws SQLException { try { - return new PaymentTransaction(connection, signature); + return new PaymentTransaction(signature); } catch (NoDataFoundException e) { return null; } @@ -114,7 +112,7 @@ public class PaymentTransaction extends Transaction { String sql = DB.formatInsertWithPlaceholders("PaymentTransactions", "signature", "sender", "recipient", "amount"); PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.signature, this.sender.getPublicKey(), this.recipient, this.amount); + DB.bindInsertPlaceholders(preparedStatement, this.signature, this.sender.getPublicKey(), this.recipient.getAddress(), this.amount); preparedStatement.execute(); } @@ -127,7 +125,7 @@ public class PaymentTransaction extends Transaction { @SuppressWarnings("unchecked") @Override - public JSONObject toJSON() { + public JSONObject toJSON() throws SQLException { JSONObject json = getBaseJSON(); json.put("sender", this.sender.getAddress()); diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 1e556583..2902e72e 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -171,34 +171,32 @@ public abstract class Transaction { /** * Get block height for this transaction in the blockchain. * - * @param connection * @return height, or 0 if not in blockchain (i.e. unconfirmed) * @throws SQLException */ - public int getHeight(Connection connection) throws SQLException { + public int getHeight() throws SQLException { if (this.signature == null) return 0; - BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(connection, this.signature); + BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature); if (blockTx == null) return 0; - return BlockChain.getBlockHeightFromSignature(connection, blockTx.getBlockSignature()); + return BlockChain.getBlockHeightFromSignature(blockTx.getBlockSignature()); } /** * Get number of confirmations for this transaction. * - * @param connection * @return confirmation count, or 0 if not in blockchain (i.e. unconfirmed) * @throws SQLException */ - public int getConfirmations(Connection connection) throws SQLException { - int ourHeight = this.getHeight(connection); + public int getConfirmations() throws SQLException { + int ourHeight = this.getHeight(); if (ourHeight == 0) return 0; - int blockChainHeight = BlockChain.getMaxHeight(connection); + int blockChainHeight = BlockChain.getMaxHeight(); return blockChainHeight - ourHeight + 1; } @@ -211,21 +209,20 @@ public abstract class Transaction { *

* Note that the transaction type is not checked against the DB's value. * - * @param connection * @param type * @param signature * @throws NoDataFoundException * if no matching row found * @throws SQLException */ - protected Transaction(Connection connection, TransactionType type, byte[] signature) throws SQLException { - ResultSet rs = DB.executeUsingBytes(connection, "SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); + protected Transaction(TransactionType type, byte[] signature) throws SQLException { + ResultSet rs = DB.executeUsingBytes("SELECT reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature); if (rs == null) throw new NoDataFoundException(); this.type = type; this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH); - this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH)); + this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2))); this.timestamp = rs.getTimestamp(3).getTime(); this.fee = rs.getBigDecimal(4).setScale(8); this.signature = signature; @@ -244,88 +241,67 @@ public abstract class Transaction { /** * Load encapsulating Block from DB, if any * - * @param connection * @return Block, or null if transaction is not in a Block * @throws SQLException */ - public Block getBlock(Connection connection) throws SQLException { + public Block getBlock() throws SQLException { if (this.signature == null) return null; - BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(connection, this.signature); + BlockTransaction blockTx = BlockTransaction.fromTransactionSignature(this.signature); if (blockTx == null) return null; - return Block.fromSignature(connection, blockTx.getBlockSignature()); + return Block.fromSignature(blockTx.getBlockSignature()); } /** * Load parent Transaction from DB via this transaction's reference. * - * @param connection * @return Transaction, or null if no parent found (which should not happen) * @throws SQLException */ - public Transaction getParent(Connection connection) throws SQLException { + public Transaction getParent() throws SQLException { if (this.reference == null) return null; - return TransactionFactory.fromSignature(connection, this.reference); + return TransactionFactory.fromSignature(this.reference); } /** * Load child Transaction from DB, if any. * - * @param connection * @return Transaction, or null if no child found * @throws SQLException */ - public Transaction getChild(Connection connection) throws SQLException { + public Transaction getChild() throws SQLException { if (this.signature == null) return null; - return TransactionFactory.fromReference(connection, this.signature); + return TransactionFactory.fromReference(this.signature); } // Converters - public abstract JSONObject toJSON(); + public abstract JSONObject toJSON() throws SQLException; /** * Produce JSON representation of common/base Transaction info. - *

- * To include info on number of confirmations, a Connection object is required. See {@link Transaction#getBaseJSON(Connection)} * * @return JSONObject + * @throws SQLException */ @SuppressWarnings("unchecked") - protected JSONObject getBaseJSON() { + protected JSONObject getBaseJSON() throws SQLException { JSONObject json = new JSONObject(); json.put("type", this.type.value); json.put("fee", this.fee.toPlainString()); json.put("timestamp", this.timestamp); - json.put("reference", Base58.encode(this.reference)); + if (this.reference != null) + json.put("reference", Base58.encode(this.reference)); json.put("signature", Base58.encode(this.signature)); - - return json; - } - - /** - * Produce JSON representation of common/base Transaction info, including number of confirmations. - *

- * Requires SQL Connection object to determine number of confirmations. - * - * @param connection - * @return JSONObject - * @throws SQLException - * @see Transaction#getBaseJSON() - */ - @SuppressWarnings("unchecked") - protected JSONObject getBaseJSON(Connection connection) throws SQLException { - JSONObject json = this.getBaseJSON(); - - json.put("confirmations", this.getConfirmations(connection)); + json.put("confirmations", this.getConfirmations()); return json; } @@ -353,11 +329,11 @@ public abstract class Transaction { return signer.sign(this.toBytesLessSignature()); } - public boolean isSignatureValid(PublicKeyAccount signer) { + public boolean isSignatureValid() { if (this.signature == null) return false; - return signer.verify(this.signature, this.toBytesLessSignature()); + return this.creator.verify(this.signature, this.toBytesLessSignature()); } public abstract ValidationResult isValid(Connection connection); diff --git a/src/qora/transaction/TransactionFactory.java b/src/qora/transaction/TransactionFactory.java index 9f43cf70..d2c0d5fb 100644 --- a/src/qora/transaction/TransactionFactory.java +++ b/src/qora/transaction/TransactionFactory.java @@ -1,6 +1,5 @@ package qora.transaction; -import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -12,30 +11,28 @@ public class TransactionFactory { /** * Load Transaction from DB using signature. * - * @param connection * @param signature * @return ? extends Transaction, or null if not found * @throws SQLException */ - public static Transaction fromSignature(Connection connection, byte[] signature) throws SQLException { - ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT type, signature FROM Transactions WHERE signature = ?", signature); - return fromResultSet(connection, resultSet); + public static Transaction fromSignature(byte[] signature) throws SQLException { + ResultSet resultSet = DB.executeUsingBytes("SELECT type, signature FROM Transactions WHERE signature = ?", signature); + return fromResultSet(resultSet); } /** * Load Transaction from DB using reference. * - * @param connection * @param reference * @return ? extends Transaction, or null if not found * @throws SQLException */ - public static Transaction fromReference(Connection connection, byte[] reference) throws SQLException { - ResultSet resultSet = DB.executeUsingBytes(connection, "SELECT type, signature FROM Transactions WHERE reference = ?", reference); - return fromResultSet(connection, resultSet); + public static Transaction fromReference(byte[] reference) throws SQLException { + ResultSet resultSet = DB.executeUsingBytes("SELECT type, signature FROM Transactions WHERE reference = ?", reference); + return fromResultSet(resultSet); } - private static Transaction fromResultSet(Connection connection, ResultSet resultSet) throws SQLException { + private static Transaction fromResultSet(ResultSet resultSet) throws SQLException { if (resultSet == null) return null; @@ -47,10 +44,10 @@ public class TransactionFactory { switch (type) { case GENESIS: - return GenesisTransaction.fromSignature(connection, signature); + return GenesisTransaction.fromSignature(signature); case PAYMENT: - return PaymentTransaction.fromSignature(connection, signature); + return PaymentTransaction.fromSignature(signature); default: return null; diff --git a/src/test/blocks.java b/src/test/blocks.java new file mode 100644 index 00000000..44aee7c7 --- /dev/null +++ b/src/test/blocks.java @@ -0,0 +1,84 @@ +package test; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import org.junit.Test; + +import database.DB; +import qora.block.Block; +import qora.block.GenesisBlock; +import qora.transaction.Transaction; +import qora.transaction.TransactionFactory; + +public class blocks extends common { + + @Test + public void testGenesisBlockTransactions() throws SQLException { + try (final Connection connection = DB.getConnection()) { + GenesisBlock block = GenesisBlock.getInstance(); + assertNotNull(block); + assertTrue(block.isSignatureValid()); + // only true if blockchain is empty + // assertTrue(block.isValid(connection)); + + List transactions = block.getTransactions(); + assertNotNull(transactions); + + for (Transaction transaction : transactions) { + assertNotNull(transaction); + assertEquals(Transaction.TransactionType.GENESIS, transaction.getType()); + assertTrue(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); + assertNull(transaction.getReference()); + assertTrue(transaction.isSignatureValid()); + assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection)); + } + + // Attempt to load first transaction directly from database + Transaction transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); + assertNotNull(transaction); + assertEquals(Transaction.TransactionType.GENESIS, transaction.getType()); + assertTrue(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); + assertNull(transaction.getReference()); + assertTrue(transaction.isSignatureValid()); + assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection)); + } + } + + @Test + public void testBlockPaymentTransactions() throws SQLException { + try (final Connection connection = DB.getConnection()) { + // Block 949 has lots of varied transactions + // Blocks 390 & 754 have only payment transactions + Block block = Block.fromHeight(754); + assertNotNull("Block 754 is required for this test", block); + assertTrue(block.isSignatureValid()); + + List transactions = block.getTransactions(); + assertNotNull(transactions); + + for (Transaction transaction : transactions) { + assertNotNull(transaction); + assertEquals(Transaction.TransactionType.PAYMENT, transaction.getType()); + assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); + assertNotNull(transaction.getReference()); + assertTrue(transaction.isSignatureValid()); + assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection)); + } + + // Attempt to load first transaction directly from database + Transaction transaction = TransactionFactory.fromSignature(transactions.get(0).getSignature()); + assertNotNull(transaction); + assertEquals(Transaction.TransactionType.PAYMENT, transaction.getType()); + assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0); + assertNotNull(transaction.getReference()); + assertTrue(transaction.isSignatureValid()); + assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection)); + } + } + +} diff --git a/src/test/common.java b/src/test/common.java index 885fae0a..bfb8eff3 100644 --- a/src/test/common.java +++ b/src/test/common.java @@ -1,21 +1,31 @@ package test; -import static org.junit.Assert.fail; - -import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import database.DB; + public class common { - public static Connection getConnection() { + @BeforeClass + public static void setConnection() throws SQLException { + DB.setUrl("jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"); + DB.open(); + + // Create/update database schema try { - return DriverManager.getConnection("jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true", "SA", ""); + updates.updateDatabase(); } catch (SQLException e) { e.printStackTrace(); - fail(); - return null; + throw e; } } + @AfterClass + public static void closeDatabase() throws SQLException { + DB.close(); + } + } diff --git a/src/test/connections.java b/src/test/connections.java index 31851e34..9ebe21ec 100644 --- a/src/test/connections.java +++ b/src/test/connections.java @@ -3,18 +3,18 @@ package test; import static org.junit.Assert.*; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; -import java.sql.Statement; import org.junit.Test; -public class connections { +import database.DB; + +public class connections extends common { @Test public void testConnection() { try { - Connection c = DriverManager.getConnection("jdbc:hsqldb:file:db/test", "SA", ""); + Connection c = DB.getConnection(); c.close(); } catch (SQLException e) { e.printStackTrace(); @@ -24,53 +24,28 @@ public class connections { @Test public void testSimultaneousConnections() { + int n_connections = 5; + Connection[] connections = new Connection[n_connections]; + try { - Connection c1 = DriverManager.getConnection("jdbc:hsqldb:file:db/test", "SA", ""); - Connection c2 = DriverManager.getConnection("jdbc:hsqldb:file:db/test", "SA", ""); - c1.close(); - c2.close(); + for (int i = 0; i < n_connections; ++i) + connections[i] = DB.getConnection(); + + // Close in same order as opening + for (int i = 0; i < n_connections; ++i) + connections[i].close(); } catch (SQLException e) { e.printStackTrace(); fail(); } } - @Test - public void testExistOnlyConnection() { - try { - Connection c = DriverManager.getConnection("jdbc:hsqldb:file:db/test;ifexists=true", "SA", ""); - c.close(); - } catch (SQLException e) { - e.printStackTrace(); - fail(); - } - } - - @Test - public void testConnectionAfterShutdown() { - try { - Connection c = DriverManager.getConnection("jdbc:hsqldb:file:db/test", "SA", ""); - Statement s = c.createStatement(); - s.execute("SHUTDOWN COMPACT"); - c.close(); - - c = DriverManager.getConnection("jdbc:hsqldb:file:db/test", "SA", ""); - c.close(); - } catch (SQLException e) { - e.printStackTrace(); - fail(); - } - } - - @Test - public void testComplexConnection() { - try { - Connection c = DriverManager.getConnection("jdbc:hsqldb:file:db/test;create=false;close_result=true;sql.strict_exec=true;sql.enforce_names=true", "SA", ""); - c.close(); - } catch (SQLException e) { - e.printStackTrace(); - fail(); - } - } + /* + * @Test public void testConnectionAfterShutdown() { try { DB.close(); } catch (SQLException e) { e.printStackTrace(); fail(); } + * + * try { Connection c = DB.getConnection(); c.close(); } catch (SQLException e) { // good return; } + * + * fail(); } + */ } diff --git a/src/test/crypto.java b/src/test/crypto.java index 0897ec3c..33746dd0 100644 --- a/src/test/crypto.java +++ b/src/test/crypto.java @@ -15,24 +15,24 @@ public class crypto { byte[] input = HashCode.fromString("00").asBytes(); byte[] digest = Crypto.digest(input); byte[] expected = HashCode.fromString("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d").asBytes(); - + assertArrayEquals(digest, expected); } - + @Test public void testCryptoDoubleDigest() { byte[] input = HashCode.fromString("00").asBytes(); byte[] digest = Crypto.doubleDigest(input); byte[] expected = HashCode.fromString("1406e05881e299367766d313e26c05564ec91bf721d31726bd6e46e60689539a").asBytes(); - + assertArrayEquals(digest, expected); } - + @Test public void testCryptoQoraAddress() { byte[] publicKey = HashCode.fromString("775ada64a48a30b3bfc4f1db16bca512d4088704975a62bde78781ce0cba90d6").asBytes(); String expected = "QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW"; - + assertEquals(expected, Crypto.toAddress(publicKey)); } diff --git a/src/test/load.java b/src/test/load.java index 154ac21f..a507577a 100644 --- a/src/test/load.java +++ b/src/test/load.java @@ -2,43 +2,28 @@ package test; import static org.junit.Assert.*; -import java.sql.Connection; import java.sql.SQLException; -import org.junit.After; -import org.junit.Before; import org.junit.Test; +import qora.block.BlockChain; import qora.transaction.PaymentTransaction; import qora.transaction.Transaction; import qora.transaction.TransactionFactory; import utils.Base58; -public class load { - - private static Connection connection; - - @Before - public void connect() throws SQLException { - connection = common.getConnection(); - } - - @After - public void disconnect() { - try { - connection.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } +public class load extends common { @Test public void testLoadPaymentTransaction() throws SQLException { + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getMaxHeight() >= 49778); + String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); - PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(connection, signature); + PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(signature); + assertNotNull(paymentTransaction); assertEquals(paymentTransaction.getSender().getAddress(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); assertEquals(paymentTransaction.getCreator().getAddress(), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); assertEquals(paymentTransaction.getRecipient().getAddress(), "QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU"); @@ -49,17 +34,19 @@ public class load { @Test public void testLoadFactory() throws SQLException { + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getMaxHeight() >= 49778); + String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); while (true) { - Transaction transaction = TransactionFactory.fromSignature(connection, signature); + Transaction transaction = TransactionFactory.fromSignature(signature); if (transaction == null) break; PaymentTransaction payment = (PaymentTransaction) transaction; - System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " - + payment.getSender().getAddress() + " to " + payment.getRecipient()); + System.out + .println(payment.getSender().getAddress() + " sent " + payment.getAmount().toString() + " QORA to " + payment.getRecipient().getAddress()); signature = payment.getReference(); } @@ -70,11 +57,11 @@ public class load { String signature58 = "1111222233334444"; byte[] signature = Base58.decode(signature58); - PaymentTransaction payment = PaymentTransaction.fromSignature(connection, signature); + PaymentTransaction payment = PaymentTransaction.fromSignature(signature); if (payment != null) { - System.out.println("Transaction " + Base58.encode(payment.getSignature()) + ": " + payment.getAmount().toString() + " QORA from " - + payment.getSender().getAddress() + " to " + payment.getRecipient()); + System.out + .println(payment.getSender().getAddress() + " sent " + payment.getAmount().toString() + " QORA to " + payment.getRecipient().getAddress()); fail(); } } diff --git a/src/test/migrate.java b/src/test/migrate.java index be5017db..33e4610b 100644 --- a/src/test/migrate.java +++ b/src/test/migrate.java @@ -13,7 +13,6 @@ import java.nio.charset.Charset; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.Statement; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; @@ -25,42 +24,22 @@ import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import com.google.common.hash.HashCode; import com.google.common.io.CharStreams; +import database.DB; +import qora.block.BlockChain; import utils.Base58; -public class migrate { +public class migrate extends common { private static final String GENESIS_ADDRESS = "QfGMeDQQUQePMpAmfLBJzgqyrM35RWxHGD"; private static final byte[] GENESIS_PUBLICKEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; - private static Connection c; - private static PreparedStatement startTransactionPStmt; - private static PreparedStatement commitPStmt; - private static Map publicKeyByAddress = new HashMap(); - @Before - public void connect() throws SQLException { - c = common.getConnection(); - startTransactionPStmt = c.prepareStatement("START TRANSACTION"); - commitPStmt = c.prepareStatement("COMMIT"); - } - - @After - public void disconnect() { - try { - c.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } - public Object fetchBlockJSON(int height) throws IOException { InputStream is; @@ -111,14 +90,6 @@ public class migrate { return output.toString(); } - public void startTransaction() throws SQLException { - startTransactionPStmt.execute(); - } - - public void commit() throws SQLException { - commitPStmt.execute(); - } - @Test public void testMigration() throws SQLException, IOException { // Genesis public key @@ -126,8 +97,7 @@ public class migrate { // Some other public keys for addresses that have never created a transaction publicKeyByAddress.put("QcDLhirHkSbR4TLYeShLzHw61B8UGTFusk", Base58.decode("HP58uWRBae654ze6ysmdyGv3qaDrr9BEk6cHv4WuiF7d")); - Statement stmt = c.createStatement(); - stmt.execute("DELETE FROM Blocks"); + Connection c = DB.getConnection(); PreparedStatement blocksPStmt = c .prepareStatement("INSERT INTO Blocks " + formatWithPlaceholders("signature", "version", "reference", "transaction_count", "total_fees", @@ -179,8 +149,10 @@ public class migrate { PreparedStatement blockTxPStmt = c .prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block_signature", "sequence", "transaction_signature")); - int height = 1; + int height = BlockChain.getMaxHeight() + 1; byte[] milestone_block = null; + System.out.println("Starting migration from block height " + height); + while (true) { JSONObject json = (JSONObject) fetchBlockJSON(height); if (json == null) @@ -191,7 +163,7 @@ public class migrate { JSONArray transactions = (JSONArray) json.get("transactions"); - startTransaction(); + DB.startTransaction(c); // Blocks: // signature, version, reference, transaction_count, total_fees, transactions_signature, height, generation, generation_target, generator, @@ -246,7 +218,7 @@ public class migrate { if (txReference != null) txPStmt.setBinaryStream(2, new ByteArrayInputStream(txReference)); else if (height == 1 && type == 1) - txPStmt.setNull(2, java.sql.Types.VARCHAR); // genesis transactions only + txPStmt.setNull(2, java.sql.Types.VARBINARY); // genesis transactions only else fail(); @@ -255,7 +227,7 @@ public class migrate { // Determine transaction "creator" from specific transaction info switch (type) { case 1: // genesis - txPStmt.setNull(4, java.sql.Types.VARCHAR); // genesis transactions only + txPStmt.setBinaryStream(4, new ByteArrayInputStream(GENESIS_PUBLICKEY)); // genesis transactions only break; case 2: // payment @@ -299,7 +271,7 @@ public class migrate { if (milestone_block != null) txPStmt.setBinaryStream(7, new ByteArrayInputStream(milestone_block)); else if (height == 1 && type == 1) - txPStmt.setNull(7, java.sql.Types.VARCHAR); // genesis transactions only + txPStmt.setNull(7, java.sql.Types.VARBINARY); // genesis transactions only else fail(); @@ -614,7 +586,7 @@ public class migrate { blockTxPStmt.execute(); blockTxPStmt.clearParameters(); - commit(); + DB.commit(c); } // new milestone block every 500 blocks? @@ -623,6 +595,9 @@ public class migrate { ++height; } + + c.close(); + System.out.println("Migration finished with new blockchain height " + BlockChain.getMaxHeight()); } } diff --git a/src/test/navigation.java b/src/test/navigation.java index b6bae133..710ac863 100644 --- a/src/test/navigation.java +++ b/src/test/navigation.java @@ -2,49 +2,31 @@ package test; import static org.junit.Assert.*; -import java.sql.Connection; import java.sql.SQLException; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import qora.block.Block; +import qora.block.BlockChain; import qora.transaction.PaymentTransaction; import utils.Base58; -public class navigation { - - private static Connection connection; - - @Before - public void connect() throws SQLException { - connection = common.getConnection(); - } - - @After - public void disconnect() { - try { - connection.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } +public class navigation extends common { @Test public void testNavigateFromTransactionToBlock() throws SQLException { + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getMaxHeight() >= 49778); + String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); System.out.println("Navigating to Block from transaction " + signature58); - PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(connection, signature); + PaymentTransaction paymentTransaction = PaymentTransaction.fromSignature(signature); + assertNotNull("Payment transaction not loaded from database", paymentTransaction); - assertNotNull(paymentTransaction); - - Block block = paymentTransaction.getBlock(connection); - - assertNotNull(block); + Block block = paymentTransaction.getBlock(); + assertNotNull("Block 49778 not loaded from database", block); System.out.println("Block " + block.getHeight() + ", signature: " + Base58.encode(block.getSignature())); diff --git a/src/test/save.java b/src/test/save.java index f2506a20..b24b1f7b 100644 --- a/src/test/save.java +++ b/src/test/save.java @@ -1,40 +1,18 @@ package test; -import static org.junit.Assert.*; - import java.math.BigDecimal; import java.sql.Connection; import java.sql.SQLException; -import java.sql.Statement; import java.time.Instant; -import org.junit.After; -import org.junit.Before; import org.junit.Test; +import database.DB; import qora.account.PublicKeyAccount; import qora.transaction.PaymentTransaction; import utils.Base58; -public class save { - - private static Connection connection; - - @Before - public void connect() throws SQLException { - connection = common.getConnection(); - Statement stmt = connection.createStatement(); - stmt.execute("SET DATABASE SQL SYNTAX MYS TRUE"); - } - - @After - public void disconnect() { - try { - connection.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } +public class save extends common { @Test public void testSavePaymentTransaction() throws SQLException { @@ -47,7 +25,9 @@ public class save { PaymentTransaction paymentTransaction = new PaymentTransaction(sender, "Qrecipient", BigDecimal.valueOf(12345L), BigDecimal.ONE, Instant.now().getEpochSecond(), reference, signature); - paymentTransaction.save(connection); + try (final Connection connection = DB.getConnection()) { + paymentTransaction.save(connection); + } } } diff --git a/src/test/signatures.java b/src/test/signatures.java index 8b7365d1..efb44d58 100644 --- a/src/test/signatures.java +++ b/src/test/signatures.java @@ -2,33 +2,14 @@ package test; import static org.junit.Assert.*; -import java.sql.Connection; import java.sql.SQLException; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import qora.block.GenesisBlock; import utils.Base58; -public class signatures { - - private static Connection connection; - - @Before - public void connect() throws SQLException { - connection = common.getConnection(); - } - - @After - public void disconnect() { - try { - connection.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } +public class signatures extends common { @Test public void testGenesisBlockSignature() throws SQLException { diff --git a/src/test/updates.java b/src/test/updates.java index a12c1045..f8576db1 100644 --- a/src/test/updates.java +++ b/src/test/updates.java @@ -7,236 +7,229 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -public class updates { +import database.DB; - private static Connection c; +public class updates extends common { - @Before - public void connect() throws SQLException { - c = common.getConnection(); - Statement stmt = c.createStatement(); - stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED"); - } - - @After - public void disconnect() { - try { - c.createStatement().execute("SHUTDOWN"); - } catch (SQLException e) { - fail(); - } - } - - public boolean databaseUpdating() throws SQLException { + public static boolean databaseUpdating() throws SQLException { int databaseVersion = fetchDatabaseVersion(); - Statement stmt = c.createStatement(); + try (final Connection c = DB.getConnection()) { + Statement stmt = c.createStatement(); - // Try not to add too many constraints as much of these checks will be performed during transaction validation - // Also some constraints might be too harsh on competing unconfirmed transactions + // Try not to add too many constraints as much of these checks will be performed during transaction validation + // Also some constraints might be too harsh on competing unconfirmed transactions - switch (databaseVersion) { - case 0: - // create from new - stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); - stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); - stmt.execute("CREATE DOMAIN BlockSignature AS VARBINARY(128)"); - stmt.execute("CREATE DOMAIN Signature AS VARBINARY(64)"); - stmt.execute("CREATE DOMAIN QoraAddress AS VARCHAR(36)"); - stmt.execute("CREATE DOMAIN QoraPublicKey AS VARBINARY(32)"); - stmt.execute("CREATE DOMAIN QoraAmount AS DECIMAL(19, 8)"); - stmt.execute("CREATE DOMAIN RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE DOMAIN NameData AS VARCHAR(4000)"); - stmt.execute("CREATE DOMAIN PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE DOMAIN PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE DOMAIN DataHash AS VARCHAR(100)"); - stmt.execute("CREATE DOMAIN AssetID AS BIGINT"); - stmt.execute("CREATE DOMAIN AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE DOMAIN AssetOrderID AS VARCHAR(100)"); - stmt.execute("CREATE DOMAIN ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE DOMAIN ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); - break; + switch (databaseVersion) { + case 0: + // create from new + stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED"); + stmt.execute("SET FILES SPACE TRUE"); + stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); + stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); + stmt.execute("CREATE DOMAIN BlockSignature AS VARBINARY(128)"); + stmt.execute("CREATE DOMAIN Signature AS VARBINARY(64)"); + stmt.execute("CREATE DOMAIN QoraAddress AS VARCHAR(36)"); + stmt.execute("CREATE DOMAIN QoraPublicKey AS VARBINARY(32)"); + stmt.execute("CREATE DOMAIN QoraAmount AS DECIMAL(19, 8)"); + stmt.execute("CREATE DOMAIN RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE DOMAIN NameData AS VARCHAR(4000)"); + stmt.execute("CREATE DOMAIN PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE DOMAIN PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE DOMAIN DataHash AS VARCHAR(100)"); + stmt.execute("CREATE DOMAIN AssetID AS BIGINT"); + stmt.execute("CREATE DOMAIN AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE DOMAIN AssetOrderID AS VARCHAR(100)"); + stmt.execute("CREATE DOMAIN ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE DOMAIN ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); + break; - case 1: - // Blocks - stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, " - + "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, " - + "height INTEGER NOT NULL, generation TIMESTAMP NOT NULL, generation_target QoraAmount NOT NULL, " - + "generator QoraPublicKey NOT NULL, generation_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); - stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); - stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); - stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); - break; + case 1: + // Blocks + stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, " + + "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, " + + "height INTEGER NOT NULL, generation TIMESTAMP NOT NULL, generation_target QoraAmount NOT NULL, " + + "generator QoraPublicKey NOT NULL, generation_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); + stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); + stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); + stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); + stmt.execute("SET TABLE Blocks NEW SPACE"); + break; - case 2: - // Generic transactions (null reference, creator and milestone_block for genesis transactions) - stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, " - + "creator QoraPublicKey, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); - stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); - stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); - stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)"); - stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); - // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) - stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, " - + "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " - + "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)"); - // Unconfirmed transactions - // Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? - stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)"); - stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)"); - // Transaction recipients - stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 2: + // Generic transactions (null reference, creator and milestone_block for genesis transactions) + stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, " + + "creator QoraPublicKey, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); + stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); + stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); + stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)"); + stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); + stmt.execute("SET TABLE Transactions NEW SPACE"); - case 3: - // Genesis Transactions - stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QoraAddress NOT NULL, " - + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) + stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, " + + "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " + + "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)"); + stmt.execute("SET TABLE BlockTransactions NEW SPACE"); - case 4: - // Payment Transactions - stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " - + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + // Unconfirmed transactions + // Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? + stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)"); + stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)"); - case 5: - // Register Name Transactions - stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "owner QoraAddress NOT NULL, data NameData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + // Transaction recipients + stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, " + + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("SET TABLE TransactionRecipients NEW SPACE"); + break; - case 6: - // Update Name Transactions - stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 3: + // Genesis Transactions + stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QoraAddress NOT NULL, " + + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), " + + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 7: - // Sell Name Transactions - stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 4: + // Payment Transactions + stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " + + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), " + + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 8: - // Cancel Sell Name Transactions - stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 5: + // Register Name Transactions + stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "owner QoraAddress NOT NULL, data NameData NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 9: - // Buy Name Transactions - stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 6: + // Update Name Transactions + stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 10: - // Create Poll Transactions - stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, poll PollName NOT NULL, " - + "description VARCHAR(4000) NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key - stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option PollOption, " - + "PRIMARY KEY (signature, option), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); - // For the future: add flag to polls to allow one or multiple votes per voter - break; + case 7: + // Sell Name Transactions + stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "amount QoraAmount NOT NULL, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 11: - // Vote On Poll Transactions - stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll PollName NOT NULL, " - + "option_index INTEGER NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 8: + // Cancel Sell Name Transactions + stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 12: - // Arbitrary/Multi-payment Transaction Payments - stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QoraPublicKey NOT NULL, " - + "amount QoraAmount NOT NULL, asset AssetID NOT NULL, " - + "PRIMARY KEY (signature, recipient, asset), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 9: + // Buy Name Transactions + stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 13: - // Arbitrary Transactions - stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, creator QoraPublicKey NOT NULL, service TINYINT NOT NULL, " - + "data_hash DataHash NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // NB: Actual data payload stored elsewhere - // For the future: data payload should be encrypted, at the very least with transaction's reference as the seed for the encryption key - break; + case 10: + // Create Poll Transactions + stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QoraPublicKey NOT NULL, poll PollName NOT NULL, " + + "description VARCHAR(4000) NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key + stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option PollOption, " + + "PRIMARY KEY (signature, option), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + // For the future: add flag to polls to allow one or multiple votes per voter + break; - case 14: - // Issue Asset Transactions - stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, creator QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, " - + "description VARCHAR(4000) NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // For the future: maybe convert quantity from BIGINT to QoraAmount, regardless of divisibility - break; + case 11: + // Vote On Poll Transactions + stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll PollName NOT NULL, " + + "option_index INTEGER NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 15: - // Transfer Asset Transactions - stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " - + "asset AssetID NOT NULL, amount QoraAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 12: + // Arbitrary/Multi-payment Transaction Payments + stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QoraPublicKey NOT NULL, " + + "amount QoraAmount NOT NULL, asset AssetID NOT NULL, " + + "PRIMARY KEY (signature, recipient, asset), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 16: - // Create Asset Order Transactions - stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, " - + "have_asset AssetID NOT NULL, have_amount QoraAmount NOT NULL, want_asset AssetID NOT NULL, want_amount QoraAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 13: + // Arbitrary Transactions + stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, creator QoraPublicKey NOT NULL, service TINYINT NOT NULL, " + + "data_hash DataHash NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // NB: Actual data payload stored elsewhere + // For the future: data payload should be encrypted, at the very least with transaction's reference as the seed for the encryption key + break; - case 17: - // Cancel Asset Order Transactions - stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, " - + "asset_order AssetOrderID NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 14: + // Issue Asset Transactions + stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, creator QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, " + + "description VARCHAR(4000) NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // For the future: maybe convert quantity from BIGINT to QoraAmount, regardless of divisibility + break; - case 18: - // Multi-payment Transactions - stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QoraPublicKey NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 15: + // Transfer Asset Transactions + stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " + + "asset AssetID NOT NULL, amount QoraAmount NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 19: - // Deploy CIYAM AT Transactions - stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QoraPublicKey NOT NULL, AT_name ATName NOT NULL, " - + "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, " - + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 16: + // Create Asset Order Transactions + stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, " + + "have_asset AssetID NOT NULL, have_amount QoraAmount NOT NULL, want_asset AssetID NOT NULL, want_amount QoraAmount NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - case 20: - // Message Transactions - stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " - + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset AssetID NOT NULL, data VARBINARY(4000) NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 17: + // Cancel Asset Order Transactions + stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QoraPublicKey NOT NULL, " + + "asset_order AssetOrderID NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; - default: - // nothing to do - return false; + case 18: + // Multi-payment Transactions + stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QoraPublicKey NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; + + case 19: + // Deploy CIYAM AT Transactions + stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QoraPublicKey NOT NULL, AT_name ATName NOT NULL, " + + "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, " + + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; + + case 20: + // Message Transactions + stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, " + + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset AssetID NOT NULL, data VARBINARY(4000) NOT NULL, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; + + default: + // nothing to do + return false; + } } // database was updated return true; } - public int fetchDatabaseVersion() throws SQLException { + public static int fetchDatabaseVersion() throws SQLException { int databaseVersion = 0; - try { + try (final Connection c = DB.getConnection()) { Statement stmt = c.createStatement(); if (stmt.execute("SELECT version FROM DatabaseInfo")) { ResultSet rs = stmt.getResultSet(); @@ -253,16 +246,22 @@ public class updates { return databaseVersion; } - public void incrementDatabaseVersion() throws SQLException { - Statement stmt = c.createStatement(); - assertFalse(stmt.execute("UPDATE DatabaseInfo SET version = version + 1")); + public static void incrementDatabaseVersion() throws SQLException { + try (final Connection c = DB.getConnection()) { + Statement stmt = c.createStatement(); + assertFalse(stmt.execute("UPDATE DatabaseInfo SET version = version + 1")); + } + } + + public static void updateDatabase() throws SQLException { + while (databaseUpdating()) + incrementDatabaseVersion(); } @Test public void testUpdates() { try { - while (databaseUpdating()) - incrementDatabaseVersion(); + updateDatabase(); } catch (SQLException e) { e.printStackTrace(); fail();