From 015f4fa725ad7f71130bb6d6795f47f5962481be Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 24 May 2018 11:26:59 +0100 Subject: [PATCH] Rework DB connection arg passing Too many calls needing Connection object in args when they're only simple database reads. Even worse, some methods had database-less counterparts with different outputs, e.g. toJSON(). Now Connections can be requested from the pool for general database read scenarios. Code paths that might perform a multi-statement database transaction still require a Connection arg passed around as the database transaction are local to that Connection. In light of above, added support for database opening, closing, setting URL. Fixed out of bounds array index bug in unknown-length version of DB.getResultSetBytes(). Fixed Block's lazy-instantiation of Transactions. Implemented Block's isSignatureValid(). Fixed bug in Crypto.isValidAddress() which was using the wrong bytes. Fix GenesisTransaction generic constructor calling super with PaymentTransaction type! Fix GenesisTransaction signature constructor using wrong column indexes from ResultSet. In Transaction, don't expect CREATOR_LENGTH bytes from database in case we're dealing with a GenesisTransaction where the creator's public key is only 8 bytes long. Improvements to unit tests, including changing migrate so it can be run repeatedly to do incremental migrations. --- src/database/DB.java | 64 ++- src/qora/block/Block.java | 103 +++-- src/qora/block/BlockChain.java | 18 +- src/qora/block/BlockFactory.java | 22 +- src/qora/block/BlockTransaction.java | 51 ++- src/qora/block/GenesisBlock.java | 28 +- src/qora/crypto/Crypto.java | 4 +- src/qora/transaction/GenesisTransaction.java | 27 +- src/qora/transaction/PaymentTransaction.java | 16 +- src/qora/transaction/Transaction.java | 72 ++-- src/qora/transaction/TransactionFactory.java | 21 +- src/test/blocks.java | 84 ++++ src/test/common.java | 26 +- src/test/connections.java | 65 +-- src/test/crypto.java | 10 +- src/test/load.java | 41 +- src/test/migrate.java | 55 +-- src/test/navigation.java | 34 +- src/test/save.java | 30 +- src/test/signatures.java | 21 +- src/test/updates.java | 395 +++++++++---------- 21 files changed, 600 insertions(+), 587 deletions(-) create mode 100644 src/test/blocks.java 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();