diff --git a/src/database/DB.java b/src/database/DB.java index 6c5ba0fd..aa2e5449 100644 --- a/src/database/DB.java +++ b/src/database/DB.java @@ -23,11 +23,28 @@ public class DB { private static JDBCPool connectionPool; private static String connectionUrl; + /** + * Open connection pool to database using prior set connection URL. + *

+ * The connection URL must be set via {@link DB#setUrl(String)} before using this call. + * + * @throws SQLException + * @see DB#setUrl(String) + */ public static void open() throws SQLException { connectionPool = new JDBCPool(); connectionPool.setUrl(connectionUrl); } + /** + * Set the database connection URL. + *

+ * Typical example: + *

+ * {@code setUrl("jdbc:hsqldb:file:db/qora")} + * + * @param url + */ public static void setUrl(String url) { connectionUrl = url; } @@ -59,11 +76,44 @@ public class DB { c.prepareStatement("ROLLBACK").execute(); } + /** + * Shutdown database and close all connections in connection pool. + *

+ * Note: any attempts to use an existing connection after this point will fail. Also, any attempts to request a connection using {@link DB#getConnection()} + * will fail. + *

+ * After this method returns, the database can be reopened using {@link DB#open()}. + * + * @throws SQLException + */ public static void close() throws SQLException { getConnection().createStatement().execute("SHUTDOWN"); connectionPool.close(0); } + /** + * Shutdown and delete database, then rebuild it. + *

+ * See {@link DB#close()} for warnings about connections. + *

+ * Note that this only rebuilds the database schema, not the data itself. + * + * @throws SQLException + */ + public static void rebuild() throws SQLException { + // Shutdown database and close any access + DB.close(); + + // Wipe files (if any) + // TODO + + // Re-open clean database + DB.open(); + + // Apply schema updates + DatabaseUpdates.updateDatabase(); + } + /** * Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of set length. * @@ -231,4 +281,24 @@ public class DB { return resultSet; } + /** + * Fetch last value of IDENTITY column after an INSERT statement. + *

+ * Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column. + *

+ * Typically used after INSERTing NULL as the IDENTIY column's value to fetch what value was actually stored by HSQLDB. + * + * @param connection + * @return Long + * @throws SQLException + */ + public static Long callIdentity(Connection connection) throws SQLException { + PreparedStatement preparedStatement = connection.prepareStatement("CALL IDENTITY()"); + ResultSet resultSet = DB.checkedExecute(preparedStatement); + if (resultSet == null) + return null; + + return resultSet.getLong(1); + } + } diff --git a/src/database/DatabaseUpdates.java b/src/database/DatabaseUpdates.java new file mode 100644 index 00000000..e65ec7f2 --- /dev/null +++ b/src/database/DatabaseUpdates.java @@ -0,0 +1,294 @@ +package database; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class DatabaseUpdates { + + /** + * Apply any incremental changes to database schema. + * + * @throws SQLException + */ + public static void updateDatabase() throws SQLException { + while (databaseUpdating()) + incrementDatabaseVersion(); + } + + /** + * Increment database's schema version. + * + * @throws SQLException + */ + private static void incrementDatabaseVersion() throws SQLException { + try (final Connection c = DB.getConnection()) { + Statement stmt = c.createStatement(); + stmt.execute("UPDATE DatabaseInfo SET version = version + 1"); + } + } + + /** + * Fetch current version of database schema. + * + * @return int, 0 if no schema yet + * @throws SQLException + */ + private static int fetchDatabaseVersion() throws SQLException { + int databaseVersion = 0; + + try (final Connection c = DB.getConnection()) { + Statement stmt = c.createStatement(); + if (stmt.execute("SELECT version FROM DatabaseInfo")) { + ResultSet rs = stmt.getResultSet(); + + if (rs.next()) + databaseVersion = rs.getInt(1); + } + } catch (SQLException e) { + // empty database + } + + return databaseVersion; + } + + /** + * Incrementally update database schema, returning whether an update happened. + * + * @return true - if a schema update happened, false otherwise + * @throws SQLException + */ + private static boolean databaseUpdating() throws SQLException { + int databaseVersion = fetchDatabaseVersion(); + + 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. + * + * Only really add "ON DELETE CASCADE" to sub-tables that store type-specific data. For example on sub-types of Transactions like + * PaymentTransactions. A counterexample would be adding "ON DELETE CASCADE" to Assets using Assets' "reference" as a foreign key referring to + * Transactions' "signature". We want to database to automatically delete complete transaction data (Transactions row and corresponding + * PaymentTransactions row), but leave deleting less related table rows (Assets) to the Java logic. + */ + + 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, generating_balance QoraAmount NOT NULL, " + + "generator QoraPublicKey NOT NULL, generator_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)"); + stmt.execute("SET TABLE Transactions NEW SPACE"); + + // 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"); + + // 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)"); + stmt.execute("SET TABLE TransactionRecipients NEW SPACE"); + 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; + + case 21: + // Assets (including QORA coin itself) + stmt.execute( + "CREATE TABLE Assets (asset AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, " + + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)"); + break; + + case 22: + // Accounts + stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset AssetID, amount QoraAmount NOT NULL, PRIMARY KEY (account, asset))"); + break; + + default: + // nothing to do + return false; + } + } + + // database was updated + return true; + } + +} diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index 69d81618..3bf5ccaa 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -1,5 +1,8 @@ package qora.account; +import java.math.BigDecimal; +import java.sql.Connection; + public class Account { public static final int ADDRESS_LENGTH = 25; @@ -24,4 +27,40 @@ public class Account { return this.getAddress().equals(((Account) b).getAddress()); } + + // Balance manipulations - "key" is asset ID, or 0 for QORA + + public BigDecimal getBalance(long key, int confirmations) { + // TODO + return null; + } + + public BigDecimal getUnconfirmedBalance(long key) { + // TODO + return null; + } + + public BigDecimal getConfirmedBalance(long key) { + // TODO + return null; + } + + public void setConfirmedBalance(Connection connection, long key, BigDecimal amount) { + // TODO + return; + } + + // Reference manipulations + + public byte[] getLastReference() { + // TODO + return null; + } + + // pass null to remove + public void setLastReference(Connection connection, byte[] reference) { + // TODO + return; + } + } diff --git a/src/qora/assets/Asset.java b/src/qora/assets/Asset.java new file mode 100644 index 00000000..52ad82ff --- /dev/null +++ b/src/qora/assets/Asset.java @@ -0,0 +1,49 @@ +package qora.assets; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import database.DB; +import qora.account.PublicKeyAccount; + +public class Asset { + + public static final long QORA = 0L; + + // Properties + private Long key; + private PublicKeyAccount owner; + private String name; + private String description; + private long quantity; + private boolean isDivisible; + private byte[] reference; + + public Asset(Long key, PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { + this.key = key; + this.owner = owner; + this.name = name; + this.description = description; + this.quantity = quantity; + this.isDivisible = isDivisible; + this.reference = reference; + } + + public Asset(PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) { + this(null, owner, name, description, quantity, isDivisible, reference); + } + + // Load/Save + + public void save(Connection connection) throws SQLException { + String sql = DB.formatInsertWithPlaceholders("Assets", "asset", "owner", "asset_name", "description", "quantity", "is_divisible", "reference"); + PreparedStatement preparedStatement = connection.prepareStatement(sql); + DB.bindInsertPlaceholders(preparedStatement, this.key, this.owner.getAddress(), this.name, this.description, this.quantity, this.isDivisible, + this.reference); + preparedStatement.execute(); + + if (this.key == null) + this.key = DB.callIdentity(connection); + } +} diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 1877fc6e..f002b04a 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -7,6 +7,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -20,6 +21,7 @@ import database.DB; import database.NoDataFoundException; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; +import qora.assets.Asset; import qora.transaction.Transaction; import qora.transaction.TransactionFactory; @@ -40,9 +42,9 @@ import qora.transaction.TransactionFactory; * In scenarios (2) and (3) this will need to be set after successful processing, * but before Block is saved into database. * - * GenerationSignature's data is: reference + generationTarget + generator's public key - * TransactionSignature's data is: generationSignature + transaction signatures - * Block signature is: generationSignature + transactionsSignature + * GeneratorSignature's data is: reference + generatingBalance + generator's public key + * TransactionSignature's data is: generatorSignature + transaction signatures + * Block signature is: generatorSignature + transactionsSignature */ public class Block { @@ -52,7 +54,7 @@ public class Block { // Columns when fetching from database private static final String DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, generation, generation_target, generator, generation_signature, " + "AT_data, AT_fees"; + + "transactions_signature, height, generation, generating_balance, generator, generator_signature, " + "AT_data, AT_fees"; // Database properties protected int version; @@ -62,9 +64,9 @@ public class Block { protected byte[] transactionsSignature; protected int height; protected long timestamp; - protected BigDecimal generationTarget; + protected BigDecimal generatingBalance; protected PublicKeyAccount generator; - protected byte[] generationSignature; + protected byte[] generatorSignature; protected byte[] atBytes; protected BigDecimal atFees; @@ -74,17 +76,17 @@ public class Block { // Property lengths for serialisation protected static final int VERSION_LENGTH = 4; protected static final int TRANSACTIONS_SIGNATURE_LENGTH = 64; - protected static final int GENERATION_SIGNATURE_LENGTH = 64; - protected static final int REFERENCE_LENGTH = GENERATION_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; + protected static final int GENERATOR_SIGNATURE_LENGTH = 64; + protected static final int REFERENCE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; protected static final int TIMESTAMP_LENGTH = 8; - protected static final int GENERATION_TARGET_LENGTH = 8; + protected static final int GENERATING_BALANCE_LENGTH = 8; protected static final int GENERATOR_LENGTH = 32; protected static final int TRANSACTION_COUNT_LENGTH = 8; - protected static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATION_TARGET_LENGTH + GENERATOR_LENGTH - + TRANSACTIONS_SIGNATURE_LENGTH + GENERATION_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH; + protected static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH + + TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH; // Other length constants - protected static final int BLOCK_SIGNATURE_LENGTH = GENERATION_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; + protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH; public static final int MAX_BLOCK_BYTES = 1048576; protected static final int TRANSACTION_SIZE_LENGTH = 4; // per transaction public static final int MAX_TRANSACTION_BYTES = MAX_BLOCK_BYTES - BASE_LENGTH; @@ -95,24 +97,25 @@ public class Block { // 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) { + protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature, + byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees) { this.version = version; this.reference = reference; this.timestamp = timestamp; - this.generationTarget = generationTarget; + this.generatingBalance = generatingBalance; this.generator = generator; - this.generationSignature = generationSignature; + this.generatorSignature = generatorSignature; this.height = 0; this.transactionCount = 0; this.transactions = new ArrayList(); - this.transactionsSignature = null; - this.totalFees = null; + this.transactionsSignature = transactionsSignature; + this.totalFees = BigDecimal.ZERO.setScale(8); this.atBytes = atBytes; this.atFees = atFees; + if (this.atFees != null) + this.totalFees = this.totalFees.add(this.atFees); } // Getters/setters @@ -129,16 +132,16 @@ public class Block { return this.timestamp; } - public BigDecimal getGenerationTarget() { - return this.generationTarget; + public BigDecimal getGeneratingBalance() { + return this.generatingBalance; } public PublicKeyAccount getGenerator() { return this.generator; } - public byte[] getGenerationSignature() { - return this.generationSignature; + public byte[] getGeneratorSignature() { + return this.generatorSignature; } public byte[] getTransactionsSignature() { @@ -146,7 +149,7 @@ public class Block { } public BigDecimal getTotalFees() { - return null; + return this.totalFees; } public int getTransactionCount() { @@ -167,11 +170,16 @@ public class Block { // More information + /** + * Return composite block signature (generatorSignature + transactionsSignature). + * + * @return byte[], or null if either component signature is null. + */ public byte[] getSignature() { - if (this.generationSignature == null || this.transactionsSignature == null) + if (this.generatorSignature == null || this.transactionsSignature == null) return null; - return Bytes.concat(this.generationSignature, this.transactionsSignature); + return Bytes.concat(this.generatorSignature, this.transactionsSignature); } public int getDataLength() { @@ -214,6 +222,8 @@ public class Block { do { byte[] transactionSignature = DB.getResultSetBytes(rs.getBinaryStream(1), Transaction.SIGNATURE_LENGTH); this.transactions.add(TransactionFactory.fromSignature(transactionSignature)); + + // No need to update totalFees as this will be loaded via the Blocks table } while (rs.next()); return this.transactions; @@ -236,9 +246,10 @@ public class Block { this.transactionsSignature = DB.getResultSetBytes(rs.getBinaryStream(5), TRANSACTIONS_SIGNATURE_LENGTH); this.height = rs.getInt(6); this.timestamp = rs.getTimestamp(7).getTime(); - this.generationTarget = rs.getBigDecimal(8); - this.generator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(9), GENERATOR_LENGTH)); - this.generationSignature = DB.getResultSetBytes(rs.getBinaryStream(10), GENERATION_SIGNATURE_LENGTH); + this.generatingBalance = rs.getBigDecimal(8); + // Note: can't use GENERATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key + this.generator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(9))); + this.generatorSignature = DB.getResultSetBytes(rs.getBinaryStream(10), GENERATOR_SIGNATURE_LENGTH); this.atBytes = DB.getResultSetBytes(rs.getBinaryStream(11)); this.atFees = rs.getBigDecimal(12); } @@ -279,15 +290,13 @@ public class Block { } protected void save(Connection connection) throws SQLException { - String sql = DB.formatInsertWithPlaceholders("Blocks", "version", "reference", "transaction_count", "total_fees", "transactions_signature", "height", - "generation", "generation_target", "generator", "generation_signature", "AT_data", "AT_fees"); + String sql = DB.formatInsertWithPlaceholders("Blocks", "signature", "version", "reference", "transaction_count", "total_fees", "transactions_signature", + "height", "generation", "generating_balance", "generator", "generator_signature", "AT_data", "AT_fees"); PreparedStatement preparedStatement = connection.prepareStatement(sql); - DB.bindInsertPlaceholders(preparedStatement, this.version, this.reference, this.transactionCount, this.totalFees, this.transactionsSignature, - this.height, this.timestamp, this.generationTarget, this.generator.getPublicKey(), this.generationSignature, this.atBytes, this.atFees); + DB.bindInsertPlaceholders(preparedStatement, this.getSignature(), this.version, this.reference, this.transactionCount, this.totalFees, + this.transactionsSignature, this.height, new Timestamp(this.timestamp), this.generatingBalance, this.generator.getPublicKey(), + this.generatorSignature, this.atBytes, this.atFees); preparedStatement.execute(); - - // Save transactions - // Save transaction-block mappings } // Navigation @@ -345,6 +354,7 @@ public class Block { // Check there is space in block // Add to block // Update transaction count + // Update totalFees // Update transactions signature return false; // no room } @@ -356,10 +366,10 @@ public class Block { private byte[] getBytesForSignature() { try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(REFERENCE_LENGTH + GENERATION_TARGET_LENGTH + GENERATOR_LENGTH); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(REFERENCE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); // Only copy the generator signature from reference, which is the first 64 bytes. - bytes.write(Arrays.copyOf(this.reference, GENERATION_SIGNATURE_LENGTH)); - bytes.write(Longs.toByteArray(this.generationTarget.longValue())); + bytes.write(Arrays.copyOf(this.reference, GENERATOR_SIGNATURE_LENGTH)); + bytes.write(Longs.toByteArray(this.generatingBalance.longValue())); // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long. bytes.write(Bytes.ensureCapacity(this.generator.getPublicKey(), GENERATOR_LENGTH, 0)); return bytes.toByteArray(); @@ -370,13 +380,13 @@ public class Block { public boolean isSignatureValid() { // Check generator's signature first - if (!this.generator.verify(this.generationSignature, getBytesForSignature())) + if (!this.generator.verify(this.generatorSignature, getBytesForSignature())) return false; // Check transactions signature - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATION_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH); try { - bytes.write(this.generationSignature); + bytes.write(this.generatorSignature); for (Transaction transaction : this.getTransactions()) { if (!transaction.isSignatureValid()) @@ -399,11 +409,36 @@ public class Block { return false; } - public void process() { - // TODO + public void process(Connection connection) throws SQLException { + // Process transactions (we'll link them to this block after saving the block itself) + List transactions = this.getTransactions(); + for (Transaction transaction : transactions) + transaction.process(connection); + + // If fees are non-zero then add fees to generator's balance + BigDecimal blockFee = this.getTotalFees(); + if (blockFee.compareTo(BigDecimal.ZERO) == 1) + this.generator.setConfirmedBalance(connection, Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); + + // Link block into blockchain by fetching signature of highest block and setting that as our reference + int blockchainHeight = BlockChain.getHeight(); + Block latestBlock = Block.fromHeight(blockchainHeight); + if (latestBlock != null) + this.reference = latestBlock.getSignature(); + this.height = blockchainHeight + 1; + this.save(connection); + + // Link transactions to this block, thus removing them from unconfirmed transactions list. + for (int sequence = 0; sequence < transactions.size(); ++sequence) { + Transaction transaction = transactions.get(sequence); + + // Link transaction to this block + BlockTransaction blockTransaction = new BlockTransaction(this.getSignature(), sequence, transaction.getSignature()); + blockTransaction.save(connection); + } } - public void orphan() { + public void orphan(Connection connection) { // TODO } diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 3a1cc3b6..9a921d57 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -5,6 +5,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import database.DB; +import qora.assets.Asset; /** * Class representing the blockchain as a whole. @@ -12,6 +13,46 @@ import database.DB; */ public class BlockChain { + /** + * Some sort start-up/initialization/checking method. + * + * @throws SQLException + */ + public static void validate() throws SQLException { + // Check first block is Genesis Block + if (!isGenesisBlockValid()) + rebuildBlockchain(); + } + + private static boolean isGenesisBlockValid() throws SQLException { + int blockchainHeight = getHeight(); + if (blockchainHeight < 1) + return false; + + Block block = Block.fromHeight(1); + if (block == null) + return false; + + return GenesisBlock.isGenesisBlock(block); + } + + private static void rebuildBlockchain() throws SQLException { + // (Re)build database + DB.rebuild(); + + try (final Connection connection = DB.getConnection()) { + // Add Genesis Block + GenesisBlock genesisBlock = GenesisBlock.getInstance(); + genesisBlock.process(connection); + + // Add QORA asset. + // NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! + Asset qoraAsset = new Asset(Asset.QORA, genesisBlock.getGenerator(), "Qora", "This is the simulated Qora asset.", 10_000_000_000L, true, + genesisBlock.getGeneratorSignature()); + qoraAsset.save(connection); + } + } + /** * Return block height from DB using signature. * @@ -33,7 +74,7 @@ public class BlockChain { * @return height, or 0 if there are no blocks in DB (not very likely). * @throws SQLException */ - public static int getMaxHeight() throws SQLException { + public static int getHeight() throws SQLException { try (final Connection connection = DB.getConnection()) { ResultSet rs = DB.checkedExecute(connection.prepareStatement("SELECT MAX(height) FROM Blocks")); if (rs == null) diff --git a/src/qora/block/BlockTransaction.java b/src/qora/block/BlockTransaction.java index f8a4f613..4f899f6b 100644 --- a/src/qora/block/BlockTransaction.java +++ b/src/qora/block/BlockTransaction.java @@ -23,7 +23,7 @@ public class BlockTransaction { // Constructors - protected BlockTransaction(byte[] blockSignature, int sequence, byte[] transactionSignature) { + public BlockTransaction(byte[] blockSignature, int sequence, byte[] transactionSignature) { this.blockSignature = blockSignature; this.sequence = sequence; this.transactionSignature = transactionSignature; diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 07aa4c27..7fc8ff8b 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -24,19 +24,18 @@ public class GenesisBlock extends Block { private static final int GENESIS_BLOCK_VERSION = 1; private static final byte[] GENESIS_REFERENCE = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; // NOTE: Neither 64 nor 128 bytes! - private static final BigDecimal GENESIS_GENERATION_TARGET = BigDecimal.valueOf(10_000_000L).setScale(8); + private static final BigDecimal GENESIS_GENERATING_BALANCE = BigDecimal.valueOf(10_000_000L).setScale(8); private static final GenesisAccount GENESIS_GENERATOR = new GenesisAccount(); private static final long GENESIS_TIMESTAMP = 1400247274336L; // QORA RELEASE: Fri May 16 13:34:34.336 2014 UTC - private static final byte[] GENESIS_GENERATION_SIGNATURE = calcSignature(); + private static final byte[] GENESIS_GENERATOR_SIGNATURE = calcSignature(); private static final byte[] GENESIS_TRANSACTIONS_SIGNATURE = calcSignature(); // 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_GENERATING_BALANCE, GENESIS_GENERATOR, GENESIS_GENERATOR_SIGNATURE, + GENESIS_TRANSACTIONS_SIGNATURE, null, null); this.height = 1; - this.transactions = new ArrayList(); // Genesis transactions @@ -174,8 +173,6 @@ public class GenesisBlock extends Block { addGenesisTransaction("QgcphUTiVHHfHg8e1LVgg5jujVES7ZDUTr", "115031531"); addGenesisTransaction("QbQk9s4j4EAxAguBhmqA8mdtTct3qGnsrx", "138348733.2"); addGenesisTransaction("QT79PhvBwE6vFzfZ4oh5wdKVsEazZuVJFy", "6360421.343"); - - this.transactionsSignature = GENESIS_TRANSACTIONS_SIGNATURE; } /** @@ -199,7 +196,7 @@ public class GenesisBlock extends Block { return false; // Validate block signature - if (!Arrays.equals(GENESIS_GENERATION_SIGNATURE, block.generationSignature)) + if (!Arrays.equals(GENESIS_GENERATOR_SIGNATURE, block.generatorSignature)) return false; // Validate transactions signature @@ -263,7 +260,7 @@ public class GenesisBlock extends Block { } /** - * Generate genesis block generation/transactions signature. + * Generate genesis block generator/transactions signature. *

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

@@ -280,7 +277,7 @@ public class GenesisBlock extends Block { try { // 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); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + GENERATING_BALANCE_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,7 +289,7 @@ public class GenesisBlock extends Block { * will break genesis block signatures! */ bytes.write(Bytes.ensureCapacity(GENESIS_REFERENCE, 64, 0)); - bytes.write(Longs.toByteArray(GENESIS_GENERATION_TARGET.longValue())); + bytes.write(Longs.toByteArray(GENESIS_GENERATING_BALANCE.longValue())); // NOTE: Genesis account's public key is only 8 bytes, not the usual 32. bytes.write(GENESIS_GENERATOR.getPublicKey()); return bytes.toByteArray(); @@ -304,7 +301,7 @@ public class GenesisBlock extends Block { @Override public boolean isSignatureValid() { // Validate block signature - if (!Arrays.equals(GENESIS_GENERATION_SIGNATURE, this.generationSignature)) + if (!Arrays.equals(GENESIS_GENERATOR_SIGNATURE, this.generatorSignature)) return false; // Validate transactions signature @@ -317,7 +314,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() != 0) + if (BlockChain.getHeight() != 0) return false; // Validate transactions diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index d32071db..b5059195 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -202,11 +202,18 @@ public class GenesisTransaction extends Transaction { return ValidationResult.OK; } - public void process() { + public void process(Connection connection) throws SQLException { // TODO + this.save(connection); + + // SET recipient's balance + // this.recipient.setConfirmedBalance(this.amount, db); + + // Set recipient's reference + // recipient.setLastReference(this.signature, db); } - public void orphan() { + public void orphan(Connection connection) { // TODO } diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 4a99da96..80d79297 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -173,11 +173,11 @@ public class PaymentTransaction extends Transaction { return ValidationResult.OK; } - public void process() { + public void process(Connection connection) { // TODO } - public void orphan() { + public void orphan(Connection connection) { // TODO } diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 4df98c24..d0018b3d 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -8,7 +8,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; -import java.time.Instant; import java.util.Arrays; import java.util.Map; import static java.util.Arrays.stream; @@ -198,7 +197,7 @@ public abstract class Transaction { if (ourHeight == 0) return 0; - int blockChainHeight = BlockChain.getMaxHeight(); + int blockChainHeight = BlockChain.getHeight(); return blockChainHeight - ourHeight + 1; } @@ -224,6 +223,7 @@ public abstract class Transaction { this.type = type; this.reference = DB.getResultSetBytes(rs.getBinaryStream(1), REFERENCE_LENGTH); + // Note: can't use CREATOR_LENGTH in case we encounter Genesis Account's short, 8-byte public key this.creator = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2))); this.timestamp = rs.getTimestamp(3).getTime(); this.fee = rs.getBigDecimal(4).setScale(8); @@ -234,7 +234,7 @@ public abstract class Transaction { String sql = DB.formatInsertWithPlaceholders("Transactions", "signature", "reference", "type", "creator", "creation", "fee", "milestone_block"); PreparedStatement preparedStatement = connection.prepareStatement(sql); DB.bindInsertPlaceholders(preparedStatement, this.signature, this.reference, this.type.value, this.creator.getPublicKey(), - Timestamp.from(Instant.ofEpochSecond(this.timestamp)), this.fee, null); + new Timestamp(this.timestamp), this.fee, null); preparedStatement.execute(); } @@ -365,8 +365,8 @@ public abstract class Transaction { public abstract ValidationResult isValid(Connection connection); - public abstract void process(); + public abstract void process(Connection connection) throws SQLException; - public abstract void orphan(); + public abstract void orphan(Connection connection); } diff --git a/src/test/blockchain.java b/src/test/blockchain.java new file mode 100644 index 00000000..ef54f5ac --- /dev/null +++ b/src/test/blockchain.java @@ -0,0 +1,16 @@ +package test; + +import java.sql.SQLException; + +import org.junit.Test; + +import qora.block.BlockChain; + +public class blockchain extends common { + + @Test + public void testRebuild() throws SQLException { + BlockChain.validate(); + } + +} diff --git a/src/test/common.java b/src/test/common.java index bfb8eff3..6f316e46 100644 --- a/src/test/common.java +++ b/src/test/common.java @@ -6,6 +6,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import database.DB; +import database.DatabaseUpdates; public class common { @@ -15,12 +16,7 @@ public class common { DB.open(); // Create/update database schema - try { - updates.updateDatabase(); - } catch (SQLException e) { - e.printStackTrace(); - throw e; - } + DatabaseUpdates.updateDatabase(); } @AfterClass diff --git a/src/test/connections.java b/src/test/connections.java index 9ebe21ec..7700f64c 100644 --- a/src/test/connections.java +++ b/src/test/connections.java @@ -40,12 +40,23 @@ public class connections extends common { } } - /* - * @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(); } - */ + @Test + public void testConnectionAfterShutdown() { + try { + DB.close(); + } catch (SQLException e) { + e.printStackTrace(); + fail(); + } + + try { + DB.open(); + Connection c = DB.getConnection(); + c.close(); + } catch (SQLException e) { + e.printStackTrace(); + fail(); + } + } } diff --git a/src/test/load.java b/src/test/load.java index a507577a..471d126f 100644 --- a/src/test/load.java +++ b/src/test/load.java @@ -16,7 +16,7 @@ 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); + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); @@ -34,7 +34,7 @@ public class load extends common { @Test public void testLoadFactory() throws SQLException { - assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getMaxHeight() >= 49778); + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); diff --git a/src/test/migrate.java b/src/test/migrate.java index 33e4610b..6af71b3e 100644 --- a/src/test/migrate.java +++ b/src/test/migrate.java @@ -101,7 +101,7 @@ public class migrate extends common { PreparedStatement blocksPStmt = c .prepareStatement("INSERT INTO Blocks " + formatWithPlaceholders("signature", "version", "reference", "transaction_count", "total_fees", - "transactions_signature", "height", "generation", "generation_target", "generator", "generation_signature", "AT_data", "AT_fees")); + "transactions_signature", "height", "generation", "generating_balance", "generator", "generator_signature", "AT_data", "AT_fees")); PreparedStatement txPStmt = c.prepareStatement( "INSERT INTO Transactions " + formatWithPlaceholders("signature", "reference", "type", "creator", "creation", "fee", "milestone_block")); @@ -149,7 +149,7 @@ public class migrate extends common { PreparedStatement blockTxPStmt = c .prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block_signature", "sequence", "transaction_signature")); - int height = BlockChain.getMaxHeight() + 1; + int height = BlockChain.getHeight() + 1; byte[] milestone_block = null; System.out.println("Starting migration from block height " + height); @@ -166,8 +166,8 @@ public class migrate extends common { DB.startTransaction(c); // Blocks: - // signature, version, reference, transaction_count, total_fees, transactions_signature, height, generation, generation_target, generator, - // generation_signature + // signature, version, reference, transaction_count, total_fees, transactions_signature, height, generation, generating_balance, generator, + // generator_signature // varchar, tinyint, varchar, int, decimal, varchar, int, timestamp, decimal, varchar, varchar byte[] blockSignature = Base58.decode((String) json.get("signature")); byte[] blockReference = Base58.decode((String) json.get("reference")); @@ -597,7 +597,7 @@ public class migrate extends common { } c.close(); - System.out.println("Migration finished with new blockchain height " + BlockChain.getMaxHeight()); + System.out.println("Migration finished with new blockchain height " + BlockChain.getHeight()); } } diff --git a/src/test/navigation.java b/src/test/navigation.java index 710ac863..aeeac9cf 100644 --- a/src/test/navigation.java +++ b/src/test/navigation.java @@ -15,7 +15,7 @@ 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); + assertTrue("Migrate old database to at least block 49778 before running this test", BlockChain.getHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); diff --git a/src/test/signatures.java b/src/test/signatures.java index efb44d58..f3413b08 100644 --- a/src/test/signatures.java +++ b/src/test/signatures.java @@ -17,7 +17,7 @@ public class signatures extends common { GenesisBlock block = GenesisBlock.getInstance(); - System.out.println("Generator: " + block.getGenerator().getAddress() + ", generation signature: " + Base58.encode(block.getGenerationSignature())); + System.out.println("Generator: " + block.getGenerator().getAddress() + ", generator signature: " + Base58.encode(block.getGeneratorSignature())); assertEquals(expected58, Base58.encode(block.getSignature())); } diff --git a/src/test/updates.java b/src/test/updates.java index f8576db1..8160e59b 100644 --- a/src/test/updates.java +++ b/src/test/updates.java @@ -1,271 +1,16 @@ package test; -import static org.junit.Assert.*; - -import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import org.junit.Test; -import database.DB; +import database.DatabaseUpdates; public class updates extends common { - public static boolean databaseUpdating() throws SQLException { - int databaseVersion = fetchDatabaseVersion(); - - 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 - - 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)"); - 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)"); - stmt.execute("SET TABLE Transactions NEW SPACE"); - - // 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"); - - // 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)"); - stmt.execute("SET TABLE TransactionRecipients NEW SPACE"); - 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 static int fetchDatabaseVersion() throws SQLException { - int databaseVersion = 0; - - try (final Connection c = DB.getConnection()) { - Statement stmt = c.createStatement(); - if (stmt.execute("SELECT version FROM DatabaseInfo")) { - ResultSet rs = stmt.getResultSet(); - assertNotNull(rs); - - assertTrue(rs.next()); - - databaseVersion = rs.getInt(1); - } - } catch (SQLException e) { - // empty database? - } - - return databaseVersion; - } - - 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 { - updateDatabase(); - } catch (SQLException e) { - e.printStackTrace(); - fail(); - } + public void testUpdates() throws SQLException { + DatabaseUpdates.updateDatabase(); } }